agents-cli-automation 1.0.6 → 1.0.7
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.
- package/README.md +127 -44
- package/package.json +1 -1
- package/src/commands/init.js +54 -15
- package/src/templates/playwright-agent-csharp.md +521 -0
- package/src/templates/playwright-agent-java.md +471 -0
- package/src/templates/playwright-agent-js.md +461 -0
- package/src/templates/playwright-agent-ts.md +1419 -0
- package/src/templates/playwright-agent.md +1353 -47
|
@@ -9,14 +9,20 @@ argument-hint: "framework requirements, e.g., 'JavaScript with UI tests'"
|
|
|
9
9
|
This agent creates a production-ready Playwright framework optimized for UI automation.
|
|
10
10
|
|
|
11
11
|
## Capabilities
|
|
12
|
-
- Playwright setup (JS / TS)
|
|
12
|
+
- Playwright setup (JS / TS) - Chromium only with latest module syntax
|
|
13
|
+
- TypeScript support with strictest config
|
|
13
14
|
- UI, component, and end-to-end testing
|
|
14
|
-
-
|
|
15
|
+
- **Playwright BDD** - Gherkin format with step definitions
|
|
16
|
+
- **Page Object Model (POM)** - Reusable page classes
|
|
17
|
+
- **Fixtures** - Pre-built browser & page fixtures
|
|
18
|
+
- **Parallel execution** - Tests run simultaneously for speed
|
|
15
19
|
- Sample tests (headless & headed modes)
|
|
16
|
-
- Scalable folder structure
|
|
20
|
+
- Scalable folder structure with best practices
|
|
17
21
|
- WebServer configuration for running tests against local servers
|
|
18
22
|
- Complete package.json with all required scripts
|
|
19
|
-
-
|
|
23
|
+
- Modern ES modules with latest imports
|
|
24
|
+
- Full TypeScript integration with types
|
|
25
|
+
- Minimal setup - Developers only write test steps & scenarios
|
|
20
26
|
|
|
21
27
|
## Prerequisites
|
|
22
28
|
- Node.js 18+ installed
|
|
@@ -24,12 +30,20 @@ This agent creates a production-ready Playwright framework optimized for UI auto
|
|
|
24
30
|
|
|
25
31
|
## Setup Instructions
|
|
26
32
|
|
|
27
|
-
### 1. Install Dependencies
|
|
33
|
+
### 1. Install Dependencies (Chromium Only + TypeScript)
|
|
28
34
|
```bash
|
|
29
35
|
npm install
|
|
36
|
+
npm install --save-dev typescript @types/node @playwright/test
|
|
37
|
+
npx playwright install chromium
|
|
30
38
|
```
|
|
31
39
|
|
|
32
|
-
### 2.
|
|
40
|
+
### 2. Install BDD & Data Handling Dependencies
|
|
41
|
+
For Playwright BDD support and test data files:
|
|
42
|
+
```bash
|
|
43
|
+
npm install --save-dev @cucumber/cucumber csv-parser js-yaml @types/js-yaml
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Required package.json Scripts
|
|
33
47
|
Ensure your `package.json` includes these scripts:
|
|
34
48
|
```json
|
|
35
49
|
{
|
|
@@ -38,76 +52,1368 @@ Ensure your `package.json` includes these scripts:
|
|
|
38
52
|
"test": "playwright test",
|
|
39
53
|
"test:headed": "playwright test --headed",
|
|
40
54
|
"test:debug": "playwright test --debug",
|
|
41
|
-
"test:ui": "playwright test --ui"
|
|
55
|
+
"test:ui": "playwright test --ui",
|
|
56
|
+
"test:bdd": "npx @cucumber/cucumber-js",
|
|
57
|
+
"test:bdd:report": "npx @cucumber/cucumber-js --format json:cucumber-report.json"
|
|
42
58
|
}
|
|
43
59
|
}
|
|
44
60
|
```
|
|
45
61
|
|
|
46
|
-
###
|
|
47
|
-
|
|
48
|
-
```javascript
|
|
62
|
+
### 4. TypeScript Configuration (`tsconfig.json`)
|
|
63
|
+
```json
|
|
49
64
|
{
|
|
65
|
+
"compilerOptions": {
|
|
66
|
+
"target": "ES2020",
|
|
67
|
+
"lib": ["ES2020"],
|
|
68
|
+
"module": "ESNext",
|
|
69
|
+
"moduleResolution": "node",
|
|
70
|
+
"esModuleInterop": true,
|
|
71
|
+
"allowSyntheticDefaultImports": true,
|
|
72
|
+
"strict": true,
|
|
73
|
+
"skipLibCheck": true,
|
|
74
|
+
"forceConsistentCasingInFileNames": true,
|
|
75
|
+
"resolveJsonModule": true,
|
|
76
|
+
"noEmit": true,
|
|
77
|
+
"declaration": true,
|
|
78
|
+
"declarationMap": true,
|
|
79
|
+
"sourceMap": true,
|
|
80
|
+
"baseUrl": ".",
|
|
81
|
+
"paths": {
|
|
82
|
+
"@pages/*": ["tests/pages/*"],
|
|
83
|
+
"@utils/*": ["tests/utils/*"],
|
|
84
|
+
"@fixtures/*": ["tests/fixtures/*"],
|
|
85
|
+
"@data/*": ["tests/data/*"]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"include": ["tests/**/*"],
|
|
89
|
+
"exclude": ["node_modules"]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 5. Project Structure (Scalable with BDD & TypeScript)
|
|
94
|
+
```
|
|
95
|
+
project/
|
|
96
|
+
├── tests/
|
|
97
|
+
│ ├── fixtures/
|
|
98
|
+
│ │ └── base.fixture.ts # Shared fixtures (TypeScript)
|
|
99
|
+
│ ├── pages/
|
|
100
|
+
│ │ ├── BasePage.ts # Base page object (TypeScript)
|
|
101
|
+
│ │ ├── LoginPage.ts # Login page object (TypeScript)
|
|
102
|
+
│ │ ├── InventoryPage.ts # Inventory page object (TypeScript)
|
|
103
|
+
│ │ └── FormPage.ts # Form page object (TypeScript)
|
|
104
|
+
│ ├── utils/ # Utility functions (TypeScript)
|
|
105
|
+
│ │ ├── dataReader.ts
|
|
106
|
+
│ │ ├── formHelper.ts
|
|
107
|
+
│ │ └── testData.ts
|
|
108
|
+
│ ├── data/ # Test data files
|
|
109
|
+
│ │ ├── users.json
|
|
110
|
+
│ │ ├── formData.csv
|
|
111
|
+
│ │ └── config.yaml
|
|
112
|
+
│ ├── features/ # BDD Gherkin scenarios
|
|
113
|
+
│ │ ├── login.feature
|
|
114
|
+
│ │ └── shopping.feature
|
|
115
|
+
│ ├── steps/ # BDD Step definitions (TypeScript)
|
|
116
|
+
│ │ ├── loginSteps.ts
|
|
117
|
+
│ │ └── shoppingSteps.ts
|
|
118
|
+
│ └── specs/
|
|
119
|
+
│ ├── saucedemo.spec.ts # Traditional tests (TypeScript)
|
|
120
|
+
│ └── formTest.spec.ts
|
|
121
|
+
├── cucumber.js # Cucumber config
|
|
122
|
+
├── playwright.config.ts # Playwright config (TypeScript)
|
|
123
|
+
├── tsconfig.json # TypeScript config
|
|
124
|
+
└── package.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 6. Cucumber Configuration (`cucumber.js` - for BDD)
|
|
128
|
+
```javascript
|
|
129
|
+
module.exports = {
|
|
130
|
+
default: {
|
|
131
|
+
paths: ['tests/features/**/*.feature'],
|
|
132
|
+
require: ['tests/steps/**/*.ts'],
|
|
133
|
+
requireModule: ['ts-node/register'],
|
|
134
|
+
format: ['progress', 'html:cucumber-report.html'],
|
|
135
|
+
parallel: 4,
|
|
136
|
+
worldParameters: {
|
|
137
|
+
appUrl: 'https://www.saucedemo.com/',
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 7. Playwright Configuration (`playwright.config.ts`)
|
|
144
|
+
```typescript
|
|
145
|
+
import { defineConfig, devices, type PlaywrightTestOptions } from '@playwright/test';
|
|
146
|
+
|
|
147
|
+
export default defineConfig<PlaywrightTestOptions>({
|
|
148
|
+
testDir: './tests/specs',
|
|
149
|
+
fullyParallel: true,
|
|
150
|
+
workers: process.env.CI ? 1 : 4,
|
|
151
|
+
retries: 1,
|
|
152
|
+
timeout: 30000,
|
|
153
|
+
expect: {
|
|
154
|
+
timeout: 5000,
|
|
155
|
+
},
|
|
156
|
+
use: {
|
|
157
|
+
browserName: 'chromium',
|
|
158
|
+
headless: true,
|
|
159
|
+
screenshot: 'only-on-failure',
|
|
160
|
+
video: 'retain-on-failure',
|
|
161
|
+
trace: 'on-first-retry',
|
|
162
|
+
},
|
|
50
163
|
webServer: {
|
|
51
164
|
command: 'npm start',
|
|
52
165
|
url: 'http://localhost:3000',
|
|
53
166
|
reuseExistingServer: !process.env.CI,
|
|
167
|
+
},
|
|
168
|
+
reporter: [
|
|
169
|
+
['html'],
|
|
170
|
+
['json', { outputFile: 'test-results/results.json' }],
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 8. Data Reader Utility (`tests/utils/dataReader.ts`) - TypeScript
|
|
176
|
+
```typescript
|
|
177
|
+
import fs from 'fs';
|
|
178
|
+
import path from 'path';
|
|
179
|
+
import csv from 'csv-parser';
|
|
180
|
+
import yaml from 'js-yaml';
|
|
181
|
+
|
|
182
|
+
const DATA_DIR = path.join(process.cwd(), 'tests', 'data');
|
|
183
|
+
|
|
184
|
+
interface DataRecord {
|
|
185
|
+
[key: string]: string | number | boolean;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export class DataReader {
|
|
189
|
+
// Read JSON file
|
|
190
|
+
static readJSON<T = DataRecord>(filename: string): T {
|
|
191
|
+
const filePath = path.join(DATA_DIR, filename);
|
|
192
|
+
const data = fs.readFileSync(filePath, 'utf-8');
|
|
193
|
+
return JSON.parse(data) as T;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Read YAML file
|
|
197
|
+
static readYAML<T = DataRecord>(filename: string): T {
|
|
198
|
+
const filePath = path.join(DATA_DIR, filename);
|
|
199
|
+
const data = fs.readFileSync(filePath, 'utf-8');
|
|
200
|
+
return yaml.load(data) as T;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Read CSV file (returns array of objects)
|
|
204
|
+
static async readCSV(filename: string): Promise<DataRecord[]> {
|
|
205
|
+
const filePath = path.join(DATA_DIR, filename);
|
|
206
|
+
return new Promise<DataRecord[]>((resolve, reject) => {
|
|
207
|
+
const records: DataRecord[] = [];
|
|
208
|
+
fs.createReadStream(filePath)
|
|
209
|
+
.pipe(csv())
|
|
210
|
+
.on('data', (data: DataRecord) => records.push(data))
|
|
211
|
+
.on('end', () => resolve(records))
|
|
212
|
+
.on('error', reject);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Get single record by ID or key
|
|
217
|
+
static getRecordByKey<T extends DataRecord>(
|
|
218
|
+
data: T[],
|
|
219
|
+
key: keyof T,
|
|
220
|
+
value: string | number
|
|
221
|
+
): T | undefined {
|
|
222
|
+
if (Array.isArray(data)) {
|
|
223
|
+
return data.find((record) => record[key] === value);
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
54
226
|
}
|
|
55
227
|
}
|
|
56
228
|
```
|
|
57
229
|
|
|
58
|
-
###
|
|
59
|
-
|
|
230
|
+
### 9. Form Helper Utility (`tests/utils/formHelper.ts`) - TypeScript
|
|
231
|
+
```typescript
|
|
232
|
+
import { Page, Locator } from '@playwright/test';
|
|
233
|
+
|
|
234
|
+
interface FormFieldConfig {
|
|
235
|
+
type: 'text' | 'textarea' | 'dropdown' | 'radio' | 'checkbox';
|
|
236
|
+
selector?: string;
|
|
237
|
+
value: string | string[] | boolean;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export class FormHelper {
|
|
241
|
+
constructor(private page: Page) {}
|
|
242
|
+
|
|
243
|
+
// Fill text input
|
|
244
|
+
async fillInput(selector: string, value: string): Promise<void> {
|
|
245
|
+
await this.page.fill(selector, value);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fill textarea
|
|
249
|
+
async fillTextArea(selector: string, value: string): Promise<void> {
|
|
250
|
+
await this.page.locator(selector).clear();
|
|
251
|
+
await this.page.locator(selector).type(value, { delay: 50 });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Select option from dropdown (not select element)
|
|
255
|
+
async selectDropdown(dropdownSelector: string, optionText: string): Promise<void> {
|
|
256
|
+
await this.page.click(dropdownSelector);
|
|
257
|
+
await this.page.click(`text="${optionText}"`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Select radio button by label text
|
|
261
|
+
async selectRadioButton(labelText: string): Promise<void> {
|
|
262
|
+
const label = this.page.locator(`label:has-text("${labelText}")`);
|
|
263
|
+
const radioId = await label.getAttribute('for');
|
|
264
|
+
if (radioId) {
|
|
265
|
+
await this.page.locator(`#${radioId}`).click();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check checkbox by label text
|
|
270
|
+
async checkCheckbox(labelText: string): Promise<void> {
|
|
271
|
+
const label = this.page.locator(`label:has-text("${labelText}")`);
|
|
272
|
+
const checkboxId = await label.getAttribute('for');
|
|
273
|
+
if (checkboxId) {
|
|
274
|
+
const checkbox = this.page.locator(`#${checkboxId}`);
|
|
275
|
+
const isChecked = await checkbox.isChecked();
|
|
276
|
+
if (!isChecked) {
|
|
277
|
+
await checkbox.click();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Uncheck checkbox by label text
|
|
283
|
+
async uncheckCheckbox(labelText: string): Promise<void> {
|
|
284
|
+
const label = this.page.locator(`label:has-text("${labelText}")`);
|
|
285
|
+
const checkboxId = await label.getAttribute('for');
|
|
286
|
+
if (checkboxId) {
|
|
287
|
+
const checkbox = this.page.locator(`#${checkboxId}`);
|
|
288
|
+
const isChecked = await checkbox.isChecked();
|
|
289
|
+
if (isChecked) {
|
|
290
|
+
await checkbox.click();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Select multiple checkboxes
|
|
296
|
+
async checkMultiple(labels: string[]): Promise<void> {
|
|
297
|
+
for (const label of labels) {
|
|
298
|
+
await this.checkCheckbox(label);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Fill form with object data
|
|
303
|
+
async fillForm(formData: Record<string, FormFieldConfig>): Promise<void> {
|
|
304
|
+
for (const field of Object.values(formData)) {
|
|
305
|
+
if (field.type === 'text' && field.selector && typeof field.value === 'string') {
|
|
306
|
+
await this.fillInput(field.selector, field.value);
|
|
307
|
+
} else if (field.type === 'textarea' && field.selector && typeof field.value === 'string') {
|
|
308
|
+
await this.fillTextArea(field.selector, field.value);
|
|
309
|
+
} else if (field.type === 'dropdown' && field.selector && typeof field.value === 'string') {
|
|
310
|
+
await this.selectDropdown(field.selector, field.value);
|
|
311
|
+
} else if (field.type === 'radio' && typeof field.value === 'string') {
|
|
312
|
+
await this.selectRadioButton(field.value);
|
|
313
|
+
} else if (field.type === 'checkbox' && field.value) {
|
|
314
|
+
if (Array.isArray(field.value)) {
|
|
315
|
+
await this.checkMultiple(field.value);
|
|
316
|
+
} else if (typeof field.value === 'string') {
|
|
317
|
+
await this.checkCheckbox(field.value);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Verify form value
|
|
324
|
+
async getInputValue(selector: string): Promise<string | null> {
|
|
325
|
+
return await this.page.inputValue(selector);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Verify radio button is selected
|
|
329
|
+
async isRadioSelected(labelText: string): Promise<boolean> {
|
|
330
|
+
const label = this.page.locator(`label:has-text("${labelText}")`);
|
|
331
|
+
const radioId = await label.getAttribute('for');
|
|
332
|
+
return radioId ? await this.page.locator(`#${radioId}`).isChecked() : false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Verify checkbox is checked
|
|
336
|
+
async isCheckboxChecked(labelText: string): Promise<boolean> {
|
|
337
|
+
const label = this.page.locator(`label:has-text("${labelText}")`);
|
|
338
|
+
const checkboxId = await label.getAttribute('for');
|
|
339
|
+
return checkboxId ? await this.page.locator(`#${checkboxId}`).isChecked() : false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 10. Test Data Manager (`tests/utils/testData.ts`) - TypeScript
|
|
345
|
+
```typescript
|
|
346
|
+
import { DataReader } from './dataReader';
|
|
347
|
+
|
|
348
|
+
interface User {
|
|
349
|
+
id: string;
|
|
350
|
+
username: string;
|
|
351
|
+
password: string;
|
|
352
|
+
email: string;
|
|
353
|
+
firstName: string;
|
|
354
|
+
lastName: string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
interface FormData {
|
|
358
|
+
scenario: string;
|
|
359
|
+
firstName: string;
|
|
360
|
+
lastName: string;
|
|
361
|
+
email: string;
|
|
362
|
+
country: string;
|
|
363
|
+
acceptTerms: string;
|
|
364
|
+
newsletter: string;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
interface Config {
|
|
368
|
+
app: {
|
|
369
|
+
url: string;
|
|
370
|
+
timeout: number;
|
|
371
|
+
};
|
|
372
|
+
users: {
|
|
373
|
+
[key: string]: {
|
|
374
|
+
username: string;
|
|
375
|
+
password: string;
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
forms: {
|
|
379
|
+
[key: string]: {
|
|
380
|
+
[field: string]: string;
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export class TestDataManager {
|
|
386
|
+
// Load user data from JSON
|
|
387
|
+
static loadUsers(): { users: User[] } {
|
|
388
|
+
return DataReader.readJSON<{ users: User[] }>('users.json');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Load form data from CSV
|
|
392
|
+
static async loadFormData(): Promise<FormData[]> {
|
|
393
|
+
return await DataReader.readCSV('formData.csv') as unknown as FormData[];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Load config from YAML
|
|
397
|
+
static loadConfig(): Config {
|
|
398
|
+
return DataReader.readYAML<Config>('config.yaml');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Get specific user by username
|
|
402
|
+
static getUserByUsername(username: string): User | undefined {
|
|
403
|
+
const data = this.loadUsers();
|
|
404
|
+
return DataReader.getRecordByKey(data.users, 'username' as keyof User, username);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Get form data by scenario name
|
|
408
|
+
static async getFormDataByScenario(scenarioName: string): Promise<FormData | undefined> {
|
|
409
|
+
const data = await this.loadFormData();
|
|
410
|
+
return DataReader.getRecordByKey(data, 'scenario' as keyof FormData, scenarioName);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### 9. Test Data Files
|
|
416
|
+
|
|
417
|
+
**`tests/data/users.json`**
|
|
418
|
+
```json
|
|
419
|
+
{
|
|
420
|
+
"users": [
|
|
421
|
+
{
|
|
422
|
+
"id": "1",
|
|
423
|
+
"username": "standard_user",
|
|
424
|
+
"password": "secret_sauce",
|
|
425
|
+
"email": "user@example.com",
|
|
426
|
+
"firstName": "John",
|
|
427
|
+
"lastName": "Doe"
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"id": "2",
|
|
431
|
+
"username": "problem_user",
|
|
432
|
+
"password": "secret_sauce",
|
|
433
|
+
"email": "problem@example.com",
|
|
434
|
+
"firstName": "Jane",
|
|
435
|
+
"lastName": "Smith"
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**`tests/data/formData.csv`**
|
|
442
|
+
```csv
|
|
443
|
+
scenario,firstName,lastName,email,country,acceptTerms,newsletter
|
|
444
|
+
registration_valid,John,Doe,john@example.com,USA,true,true
|
|
445
|
+
registration_minimal,Jane,Smith,jane@example.com,Canada,true,false
|
|
446
|
+
registration_eu,Pierre,Martin,pierre@example.com,France,true,true
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**`tests/data/config.yaml`**
|
|
450
|
+
```yaml
|
|
451
|
+
app:
|
|
452
|
+
url: https://www.saucedemo.com/
|
|
453
|
+
timeout: 30000
|
|
454
|
+
|
|
455
|
+
users:
|
|
456
|
+
standard:
|
|
457
|
+
username: standard_user
|
|
458
|
+
password: secret_sauce
|
|
459
|
+
admin:
|
|
460
|
+
username: admin
|
|
461
|
+
password: admin123
|
|
462
|
+
|
|
463
|
+
forms:
|
|
464
|
+
registration:
|
|
465
|
+
firstName: '#firstName'
|
|
466
|
+
lastName: '#lastName'
|
|
467
|
+
email: '#email'
|
|
468
|
+
country: '.country-dropdown'
|
|
469
|
+
acceptTerms: '#terms'
|
|
470
|
+
newsletter: '#newsletter'
|
|
60
471
|
|
|
61
|
-
|
|
62
|
-
**Credentials:**
|
|
63
|
-
- Username: `standard_user`
|
|
64
|
-
- Password: `secret_sauce`
|
|
472
|
+
```
|
|
65
473
|
|
|
66
|
-
|
|
474
|
+
### 10. Enhanced Page Object with Form Helper (`tests/pages/FormPage.js`)
|
|
67
475
|
```javascript
|
|
68
|
-
import {
|
|
476
|
+
import { BasePage } from './BasePage';
|
|
477
|
+
import { FormHelper } from '../utils/formHelper.js';
|
|
478
|
+
import { TestDataManager } from '../utils/testData.js';
|
|
479
|
+
|
|
480
|
+
export class FormPage extends BasePage {
|
|
481
|
+
constructor(page) {
|
|
482
|
+
super(page);
|
|
483
|
+
this.formHelper = new FormHelper(page);
|
|
484
|
+
this.config = TestDataManager.loadConfig();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async fillRegistrationForm(testDataKey) {
|
|
488
|
+
const formData = await TestDataManager.getFormDataByScenario(testDataKey);
|
|
489
|
+
|
|
490
|
+
const formConfig = {
|
|
491
|
+
firstName: {
|
|
492
|
+
type: 'text',
|
|
493
|
+
selector: this.config.forms.registration.firstName,
|
|
494
|
+
value: formData.firstName
|
|
495
|
+
},
|
|
496
|
+
lastName: {
|
|
497
|
+
type: 'text',
|
|
498
|
+
selector: this.config.forms.registration.lastName,
|
|
499
|
+
value: formData.lastName
|
|
500
|
+
},
|
|
501
|
+
email: {
|
|
502
|
+
type: 'text',
|
|
503
|
+
selector: this.config.forms.registration.email,
|
|
504
|
+
value: formData.email
|
|
505
|
+
},
|
|
506
|
+
country: {
|
|
507
|
+
type: 'dropdown',
|
|
508
|
+
selector: this.config.forms.registration.country,
|
|
509
|
+
value: formData.country
|
|
510
|
+
},
|
|
511
|
+
acceptTerms: {
|
|
512
|
+
type: 'checkbox',
|
|
513
|
+
value: 'I accept the terms'
|
|
514
|
+
},
|
|
515
|
+
newsletter: {
|
|
516
|
+
type: 'checkbox',
|
|
517
|
+
value: formData.newsletter === 'true' ? 'Subscribe to newsletter' : null
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
for (const field of Object.values(formConfig)) {
|
|
522
|
+
if (field.value === null) continue;
|
|
523
|
+
|
|
524
|
+
if (field.type === 'text') {
|
|
525
|
+
await this.formHelper.fillInput(field.selector, field.value);
|
|
526
|
+
} else if (field.type === 'dropdown') {
|
|
527
|
+
await this.formHelper.selectDropdown(field.selector, field.value);
|
|
528
|
+
} else if (field.type === 'checkbox') {
|
|
529
|
+
await this.formHelper.checkCheckbox(field.value);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### 11. Enhanced Page Object with Form Helper (`tests/pages/FormPage.ts`) - TypeScript
|
|
537
|
+
```typescript
|
|
538
|
+
import { Page } from '@playwright/test';
|
|
539
|
+
import { BasePage } from './BasePage';
|
|
540
|
+
import { FormHelper } from '@utils/formHelper';
|
|
541
|
+
import { TestDataManager } from '@utils/testData';
|
|
542
|
+
|
|
543
|
+
export class FormPage extends BasePage {
|
|
544
|
+
readonly formHelper: FormHelper;
|
|
545
|
+
private config: any;
|
|
546
|
+
|
|
547
|
+
constructor(page: Page) {
|
|
548
|
+
super(page);
|
|
549
|
+
this.formHelper = new FormHelper(page);
|
|
550
|
+
this.config = TestDataManager.loadConfig();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async fillRegistrationForm(testDataKey: string): Promise<void> {
|
|
554
|
+
const formData = await TestDataManager.getFormDataByScenario(testDataKey);
|
|
555
|
+
if (!formData) throw new Error(`Form data not found for: ${testDataKey}`);
|
|
556
|
+
|
|
557
|
+
const formConfig = {
|
|
558
|
+
firstName: {
|
|
559
|
+
type: 'text' as const,
|
|
560
|
+
selector: this.config.forms.registration.firstName,
|
|
561
|
+
value: formData.firstName
|
|
562
|
+
},
|
|
563
|
+
lastName: {
|
|
564
|
+
type: 'text' as const,
|
|
565
|
+
selector: this.config.forms.registration.lastName,
|
|
566
|
+
value: formData.lastName
|
|
567
|
+
},
|
|
568
|
+
email: {
|
|
569
|
+
type: 'text' as const,
|
|
570
|
+
selector: this.config.forms.registration.email,
|
|
571
|
+
value: formData.email
|
|
572
|
+
},
|
|
573
|
+
country: {
|
|
574
|
+
type: 'dropdown' as const,
|
|
575
|
+
selector: this.config.forms.registration.country,
|
|
576
|
+
value: formData.country
|
|
577
|
+
},
|
|
578
|
+
acceptTerms: {
|
|
579
|
+
type: 'checkbox' as const,
|
|
580
|
+
value: 'I accept the terms'
|
|
581
|
+
},
|
|
582
|
+
newsletter: {
|
|
583
|
+
type: 'checkbox' as const,
|
|
584
|
+
value: formData.newsletter === 'true' ? 'Subscribe to newsletter' : null
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
for (const field of Object.values(formConfig)) {
|
|
589
|
+
if (field.value === null) continue;
|
|
590
|
+
|
|
591
|
+
if (field.type === 'text') {
|
|
592
|
+
await this.formHelper.fillInput(field.selector!, field.value as string);
|
|
593
|
+
} else if (field.type === 'dropdown') {
|
|
594
|
+
await this.formHelper.selectDropdown(field.selector!, field.value as string);
|
|
595
|
+
} else if (field.type === 'checkbox') {
|
|
596
|
+
await this.formHelper.checkCheckbox(field.value as string);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
69
602
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
603
|
+
### 12. Fixtures Setup (`tests/fixtures/base.fixture.ts`) - TypeScript
|
|
604
|
+
```typescript
|
|
605
|
+
import { test as base, expect } from '@playwright/test';
|
|
606
|
+
import { LoginPage } from '@pages/LoginPage';
|
|
607
|
+
import { InventoryPage } from '@pages/InventoryPage';
|
|
608
|
+
import { FormPage } from '@pages/FormPage';
|
|
609
|
+
|
|
610
|
+
type AuthFixtures = {
|
|
611
|
+
loginPage: LoginPage;
|
|
612
|
+
inventoryPage: InventoryPage;
|
|
613
|
+
formPage: FormPage;
|
|
614
|
+
authenticatedUser: { page: any; loginPage: LoginPage };
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
export const test = base.extend<AuthFixtures>({
|
|
618
|
+
loginPage: async ({ page }, use) => {
|
|
619
|
+
const loginPage = new LoginPage(page);
|
|
620
|
+
await use(loginPage);
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
inventoryPage: async ({ page }, use) => {
|
|
624
|
+
const inventoryPage = new InventoryPage(page);
|
|
625
|
+
await use(inventoryPage);
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
formPage: async ({ page }, use) => {
|
|
629
|
+
const formPage = new FormPage(page);
|
|
630
|
+
await use(formPage);
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
authenticatedUser: async ({ page, loginPage }, use) => {
|
|
634
|
+
await loginPage.goto();
|
|
635
|
+
await loginPage.login('standard_user', 'secret_sauce');
|
|
636
|
+
await use({ page, loginPage });
|
|
637
|
+
},
|
|
76
638
|
});
|
|
77
639
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
640
|
+
export { expect };
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 13. Base Page (`tests/pages/BasePage.ts`) - TypeScript
|
|
644
|
+
```typescript
|
|
645
|
+
import { Page, Locator } from '@playwright/test';
|
|
646
|
+
|
|
647
|
+
export class BasePage {
|
|
648
|
+
protected readonly page: Page;
|
|
649
|
+
|
|
650
|
+
constructor(page: Page) {
|
|
651
|
+
this.page = page;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async navigateTo(url: string): Promise<void> {
|
|
655
|
+
await this.page.goto(url);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async click(selector: string): Promise<void> {
|
|
659
|
+
await this.page.click(selector);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async fill(selector: string, text: string): Promise<void> {
|
|
663
|
+
await this.page.fill(selector, text);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async getText(selector: string): Promise<string | null> {
|
|
667
|
+
return await this.page.textContent(selector);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async getLocator(selector: string): Locator {
|
|
671
|
+
return this.page.locator(selector);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async waitForElement(selector: string, timeout: number = 5000): Promise<void> {
|
|
675
|
+
await this.page.waitForSelector(selector, { timeout });
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async isVisible(selector: string): Promise<boolean> {
|
|
679
|
+
return await this.page.locator(selector).isVisible();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### 14. Page Object Model (`tests/pages/LoginPage.ts`) - TypeScript
|
|
685
|
+
```typescript
|
|
686
|
+
import { BasePage } from './BasePage';
|
|
687
|
+
|
|
688
|
+
export class LoginPage extends BasePage {
|
|
689
|
+
private readonly usernameField = '[data-test="username"]';
|
|
690
|
+
private readonly passwordField = '[data-test="password"]';
|
|
691
|
+
private readonly loginButton = '[data-test="login-button"]';
|
|
692
|
+
private readonly appUrl = 'https://www.saucedemo.com/';
|
|
693
|
+
|
|
694
|
+
async goto(): Promise<void> {
|
|
695
|
+
await this.page.goto(this.appUrl);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async login(username: string, password: string): Promise<void> {
|
|
699
|
+
await this.page.fill(this.usernameField, username);
|
|
700
|
+
await this.page.fill(this.passwordField, password);
|
|
701
|
+
await this.page.click(this.loginButton);
|
|
702
|
+
await this.page.waitForURL('**/inventory.html');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async isLoaded(): Promise<boolean> {
|
|
706
|
+
return await this.page.locator(this.usernameField).isVisible();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### 15. Inventory Page (`tests/pages/InventoryPage.ts`) - TypeScript
|
|
712
|
+
```typescript
|
|
713
|
+
import { BasePage } from './BasePage';
|
|
714
|
+
|
|
715
|
+
export class InventoryPage extends BasePage {
|
|
716
|
+
private readonly cartBadge = '[data-test="shopping-cart-badge"]';
|
|
717
|
+
private readonly addToCartBtn = '[data-test="add-to-cart-sauce-labs-backpack"]';
|
|
718
|
+
private readonly menuButton = '[data-test="bm-menu-button"]';
|
|
719
|
+
private readonly logoutLink = '[data-test="logout-sidebar-link"]';
|
|
720
|
+
|
|
721
|
+
async addProductToCart(): Promise<void> {
|
|
722
|
+
await this.click(this.addToCartBtn);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async getCartCount(): Promise<string | null> {
|
|
726
|
+
return await this.getText(this.cartBadge);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async isProductAdded(): Promise<string | null> {
|
|
730
|
+
await this.waitForElement(this.cartBadge);
|
|
731
|
+
return await this.getText(this.cartBadge);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async logout(): Promise<void> {
|
|
735
|
+
await this.click(this.menuButton);
|
|
736
|
+
await this.click(this.logoutLink);
|
|
737
|
+
await this.page.waitForURL('https://www.saucedemo.com/');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### 16. Clean Test File with TypeScript (`tests/specs/saucedemo.spec.ts`)
|
|
743
|
+
```typescript
|
|
744
|
+
import { test, expect } from '@fixtures/base.fixture';
|
|
745
|
+
|
|
746
|
+
test.describe('SauceDemo E2E Tests', () => {
|
|
747
|
+
test('Login with valid credentials', async ({ loginPage, page }) => {
|
|
748
|
+
await loginPage.goto();
|
|
749
|
+
await loginPage.login('standard_user', 'secret_sauce');
|
|
750
|
+
expect(page.url()).toContain('inventory.html');
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test('Add product to cart', async ({ authenticatedUser, inventoryPage }) => {
|
|
754
|
+
await inventoryPage.addProductToCart();
|
|
755
|
+
const cartCount = await inventoryPage.getCartCount();
|
|
756
|
+
expect(cartCount).toBe('1');
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test('Logout from inventory', async ({ authenticatedUser, inventoryPage, page }) => {
|
|
760
|
+
await inventoryPage.logout();
|
|
761
|
+
expect(page.url()).toBe('https://www.saucedemo.com/');
|
|
762
|
+
});
|
|
85
763
|
});
|
|
86
764
|
```
|
|
87
765
|
|
|
88
|
-
###
|
|
766
|
+
### 17. Form Test with Data (`tests/specs/formTest.spec.ts`)
|
|
767
|
+
```typescript
|
|
768
|
+
import { test, expect } from '@fixtures/base.fixture';
|
|
769
|
+
import { TestDataManager } from '@utils/testData';
|
|
770
|
+
|
|
771
|
+
test.describe('Form Registration Tests with TypeScript', () => {
|
|
772
|
+
test('Register with JSON user data', async ({ formPage, page }) => {
|
|
773
|
+
const user = TestDataManager.getUserByUsername('standard_user');
|
|
774
|
+
expect(user).toBeDefined();
|
|
775
|
+
|
|
776
|
+
await page.goto('https://example.com/register');
|
|
777
|
+
await formPage.formHelper.fillInput('#firstName', user!.firstName);
|
|
778
|
+
await formPage.formHelper.fillInput('#lastName', user!.lastName);
|
|
779
|
+
await formPage.formHelper.fillInput('#email', user!.email);
|
|
780
|
+
|
|
781
|
+
await page.click('button[type="submit"]');
|
|
782
|
+
await expect(page).toHaveURL('**/success');
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test.describe.configure({ mode: 'parallel' });
|
|
786
|
+
|
|
787
|
+
test('Register with CSV test data - Valid', async ({ formPage, page }) => {
|
|
788
|
+
await page.goto('https://example.com/register');
|
|
789
|
+
await formPage.fillRegistrationForm('registration_valid');
|
|
790
|
+
await page.click('button[type="submit"]');
|
|
791
|
+
await expect(page).toHaveURL('**/success');
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test('Register with CSV test data - Minimal', async ({ formPage, page }) => {
|
|
795
|
+
await page.goto('https://example.com/register');
|
|
796
|
+
await formPage.fillRegistrationForm('registration_minimal');
|
|
797
|
+
await page.click('button[type="submit"]');
|
|
798
|
+
await expect(page).toHaveURL('**/success');
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### 18. Run Tests
|
|
89
804
|
```bash
|
|
90
|
-
# Run all tests
|
|
805
|
+
# Run all tests in parallel (4 workers)
|
|
91
806
|
npm test
|
|
92
807
|
|
|
93
|
-
# Run
|
|
808
|
+
# Run specific test file
|
|
809
|
+
npm test tests/specs/saucedemo.spec.ts
|
|
810
|
+
|
|
811
|
+
# Run tests with UI (single worker)
|
|
94
812
|
npm run test:headed
|
|
95
813
|
|
|
96
814
|
# Debug mode
|
|
97
815
|
npm run test:debug
|
|
816
|
+
|
|
817
|
+
# Run with custom worker count
|
|
818
|
+
npm test -- --workers=2
|
|
819
|
+
|
|
820
|
+
# Run BDD scenarios
|
|
821
|
+
npm run test:bdd
|
|
822
|
+
|
|
823
|
+
# Run BDD scenarios with JSON report
|
|
824
|
+
npm run test:bdd:report
|
|
825
|
+
|
|
826
|
+
# Run specific BDD feature
|
|
827
|
+
npx @cucumber/cucumber-js tests/features/login.feature
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### 19. BDD Feature File (`tests/features/login.feature`)
|
|
831
|
+
```gherkin
|
|
832
|
+
Feature: Login to SauceDemo
|
|
833
|
+
As a user
|
|
834
|
+
I want to login to the application
|
|
835
|
+
So that I can access the inventory
|
|
836
|
+
|
|
837
|
+
Background:
|
|
838
|
+
Given I navigate to the SauceDemo application
|
|
839
|
+
|
|
840
|
+
Scenario: User logs in with valid credentials
|
|
841
|
+
When I login with username "standard_user" and password "secret_sauce"
|
|
842
|
+
Then I should see the inventory page
|
|
843
|
+
And the page title should contain "Inventory"
|
|
844
|
+
|
|
845
|
+
Scenario: User logs in and adds product to cart
|
|
846
|
+
When I login with username "standard_user" and password "secret_sauce"
|
|
847
|
+
And I add "Sauce Labs Backpack" to cart
|
|
848
|
+
Then the cart count should show "1"
|
|
849
|
+
And the product should be in the cart
|
|
850
|
+
|
|
851
|
+
Scenario: User can logout
|
|
852
|
+
When I login with username "standard_user" and password "secret_sauce"
|
|
853
|
+
And I click the menu button
|
|
854
|
+
And I click logout
|
|
855
|
+
Then I should be back on the login page
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
### 20. BDD Step Definitions with TypeScript (`tests/steps/loginSteps.ts`)
|
|
859
|
+
```typescript
|
|
860
|
+
import { Given, When, Then, Before, After } from '@cucumber/cucumber';
|
|
861
|
+
import { chromium, Browser, Page } from '@playwright/test';
|
|
862
|
+
import { LoginPage } from '@pages/LoginPage';
|
|
863
|
+
import { InventoryPage } from '@pages/InventoryPage';
|
|
864
|
+
|
|
865
|
+
let browser: Browser;
|
|
866
|
+
let page: Page;
|
|
867
|
+
let loginPage: LoginPage;
|
|
868
|
+
let inventoryPage: InventoryPage;
|
|
869
|
+
|
|
870
|
+
Before(async function() {
|
|
871
|
+
browser = await chromium.launch();
|
|
872
|
+
page = await browser.newPage();
|
|
873
|
+
loginPage = new LoginPage(page);
|
|
874
|
+
inventoryPage = new InventoryPage(page);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
After(async function() {
|
|
878
|
+
await page.close();
|
|
879
|
+
await browser.close();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
Given('I navigate to the SauceDemo application', async function() {
|
|
883
|
+
await loginPage.goto();
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
When('I login with username {string} and password {string}', async function(username: string, password: string) {
|
|
887
|
+
await loginPage.login(username, password);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
Then('I should see the inventory page', async function() {
|
|
891
|
+
await page.waitForURL('**/inventory.html');
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
Then('the page title should contain {string}', async function(title: string) {
|
|
895
|
+
const pageTitle = await page.title();
|
|
896
|
+
if (!pageTitle.includes(title)) {
|
|
897
|
+
throw new Error(`Expected title to contain "${title}" but got "${pageTitle}"`);
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
When('I add {string} to cart', async function(productName: string) {
|
|
902
|
+
const addBtn = '[data-test="add-to-cart-sauce-labs-backpack"]';
|
|
903
|
+
await page.click(addBtn);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
Then('the cart count should show {string}', async function(count: string) {
|
|
907
|
+
const cartCount = await inventoryPage.getCartCount();
|
|
908
|
+
if (cartCount !== count) {
|
|
909
|
+
throw new Error(`Expected cart count "${count}" but got "${cartCount}"`);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
When('I click the menu button', async function() {
|
|
914
|
+
await page.click('[data-test="bm-menu-button"]');
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
When('I click logout', async function() {
|
|
918
|
+
await page.click('[data-test="logout-sidebar-link"]');
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
Then('I should be back on the login page', async function() {
|
|
922
|
+
await page.waitForURL('https://www.saucedemo.com/');
|
|
923
|
+
});
|
|
98
924
|
```
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test('Logout from inventory', async ({ authenticatedUser, page }) => {
|
|
928
|
+
await page.click('[data-test="bm-menu-button"]');
|
|
929
|
+
await page.click('[data-test="logout-sidebar-link"]');
|
|
930
|
+
await expect(page).toHaveURL('https://www.saucedemo.com/');
|
|
931
|
+
});
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
### 17. BDD Feature File (`tests/features/login.feature`)
|
|
935
|
+
```gherkin
|
|
936
|
+
Feature: Login to SauceDemo
|
|
937
|
+
As a user
|
|
938
|
+
I want to login to the application
|
|
939
|
+
So that I can access the inventory
|
|
940
|
+
|
|
941
|
+
Background:
|
|
942
|
+
Given I navigate to the SauceDemo application
|
|
943
|
+
|
|
944
|
+
Scenario: User logs in with valid credentials
|
|
945
|
+
When I login with username "standard_user" and password "secret_sauce"
|
|
946
|
+
Then I should see the inventory page
|
|
947
|
+
And the page title should contain "Inventory"
|
|
948
|
+
|
|
949
|
+
Scenario: User logs in and adds product to cart
|
|
950
|
+
When I login with username "standard_user" and password "secret_sauce"
|
|
951
|
+
And I add "Sauce Labs Backpack" to cart
|
|
952
|
+
Then the cart count should show "1"
|
|
953
|
+
And the product should be in the cart
|
|
954
|
+
|
|
955
|
+
Scenario: User can logout
|
|
956
|
+
When I login with username "standard_user" and password "secret_sauce"
|
|
957
|
+
And I click the menu button
|
|
958
|
+
And I click logout
|
|
959
|
+
Then I should be back on the login page
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
### 18. BDD Shopping Feature (`tests/features/shopping.feature`)
|
|
963
|
+
```gherkin
|
|
964
|
+
Feature: Shopping Cart
|
|
965
|
+
As a user
|
|
966
|
+
I want to manage my shopping cart
|
|
967
|
+
So that I can purchase items
|
|
968
|
+
|
|
969
|
+
Background:
|
|
970
|
+
Given I am logged into the application
|
|
971
|
+
|
|
972
|
+
Scenario: Add single item to cart
|
|
973
|
+
When I add "Sauce Labs Backpack" to cart
|
|
974
|
+
Then the cart count should show "1"
|
|
975
|
+
|
|
976
|
+
Scenario: Add multiple items to cart
|
|
977
|
+
When I add "Sauce Labs Backpack" to cart
|
|
978
|
+
And I add "Sauce Labs Bike Light" to cart
|
|
979
|
+
Then the cart count should show "2"
|
|
980
|
+
|
|
981
|
+
Scenario: Remove item from cart
|
|
982
|
+
When I add "Sauce Labs Backpack" to cart
|
|
983
|
+
And I remove "Sauce Labs Backpack" from cart
|
|
984
|
+
Then the cart should be empty
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### 19. BDD Step Definitions (`tests/steps/loginSteps.js`)
|
|
988
|
+
```javascript
|
|
989
|
+
import { Given, When, Then, Before, After } from '@cucumber/cucumber';
|
|
990
|
+
import { chromium } from 'playwright';
|
|
991
|
+
import { LoginPage } from '../pages/LoginPage';
|
|
992
|
+
import { InventoryPage } from '../pages/InventoryPage';
|
|
993
|
+
|
|
994
|
+
let browser;
|
|
995
|
+
let page;
|
|
996
|
+
let loginPage;
|
|
997
|
+
let inventoryPage;
|
|
998
|
+
|
|
999
|
+
Before(async function() {
|
|
1000
|
+
browser = await chromium.launch();
|
|
1001
|
+
page = await browser.newPage();
|
|
1002
|
+
loginPage = new LoginPage(page);
|
|
1003
|
+
inventoryPage = new InventoryPage(page);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
After(async function() {
|
|
1007
|
+
await page.close();
|
|
1008
|
+
await browser.close();
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
Given('I navigate to the SauceDemo application', async function() {
|
|
1012
|
+
await loginPage.goto();
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
When('I login with username {string} and password {string}', async function(username, password) {
|
|
1016
|
+
await loginPage.login(username, password);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
Then('I should see the inventory page', async function() {
|
|
1020
|
+
await page.waitForURL('**/inventory.html');
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
Then('the page title should contain {string}', async function(title) {
|
|
1024
|
+
const pageTitle = await page.title();
|
|
1025
|
+
if (!pageTitle.includes(title)) {
|
|
1026
|
+
throw new Error(`Expected title to contain "${title}" but got "${pageTitle}"`);
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
When('I add {string} to cart', async function(productName) {
|
|
1031
|
+
const addBtn = '[data-test="add-to-cart-sauce-labs-backpack"]';
|
|
1032
|
+
await page.click(addBtn);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
Then('the cart count should show {string}', async function(count) {
|
|
1036
|
+
const cartCount = await inventoryPage.getCartCount();
|
|
1037
|
+
if (cartCount !== count) {
|
|
1038
|
+
throw new Error(`Expected cart count "${count}" but got "${cartCount}"`);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
When('I click the menu button', async function() {
|
|
1043
|
+
await page.click('[data-test="bm-menu-button"]');
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
When('I click logout', async function() {
|
|
1047
|
+
await page.click('[data-test="logout-sidebar-link"]');
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
Then('I should be back on the login page', async function() {
|
|
1051
|
+
await page.waitForURL('https://www.saucedemo.com/');
|
|
1052
|
+
});
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
### 20. BDD Shopping Steps (`tests/steps/shoppingSteps.js`)
|
|
1056
|
+
```javascript
|
|
1057
|
+
import { Given, When, Then } from '@cucumber/cucumber';
|
|
1058
|
+
import { LoginPage } from '../pages/LoginPage';
|
|
1059
|
+
|
|
1060
|
+
let page;
|
|
1061
|
+
let loginPage;
|
|
1062
|
+
|
|
1063
|
+
Given('I am logged into the application', async function() {
|
|
1064
|
+
const { page: gherkinPage } = this;
|
|
1065
|
+
page = gherkinPage;
|
|
1066
|
+
loginPage = new LoginPage(page);
|
|
1067
|
+
await loginPage.goto();
|
|
1068
|
+
await loginPage.login('standard_user', 'secret_sauce');
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
When('I remove {string} from cart', async function(productName) {
|
|
1072
|
+
const removeBtn = '[data-test="remove-sauce-labs-backpack"]';
|
|
1073
|
+
await page.click(removeBtn);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
Then('the cart should be empty', async function() {
|
|
1077
|
+
const cartBadge = page.locator('[data-test="shopping-cart-badge"]');
|
|
1078
|
+
const isHidden = await cartBadge.isHidden().catch(() => true);
|
|
1079
|
+
if (!isHidden) {
|
|
1080
|
+
throw new Error('Cart is not empty');
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
Then('the product should be in the cart', async function() {
|
|
1085
|
+
const badge = await page.locator('[data-test="shopping-cart-badge"]').isVisible();
|
|
1086
|
+
if (!badge) {
|
|
1087
|
+
throw new Error('Product not found in cart');
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
### 21. Run Tests
|
|
1093
|
+
```bash
|
|
1094
|
+
# Run all tests in parallel (4 workers)
|
|
1095
|
+
npm test
|
|
1096
|
+
|
|
1097
|
+
# Run specific test file
|
|
1098
|
+
npm test tests/specs/saucedemo.spec.js
|
|
1099
|
+
|
|
1100
|
+
# Run tests with UI (single worker)
|
|
1101
|
+
npm run test:headed
|
|
1102
|
+
|
|
1103
|
+
# Debug mode
|
|
1104
|
+
npm run test:debug
|
|
1105
|
+
|
|
1106
|
+
# Run with custom worker count
|
|
1107
|
+
npm test -- --workers=2
|
|
1108
|
+
|
|
1109
|
+
# Run BDD scenarios
|
|
1110
|
+
npm run test:bdd
|
|
1111
|
+
|
|
1112
|
+
# Run BDD scenarios with JSON report
|
|
1113
|
+
npm run test:bdd:report
|
|
1114
|
+
|
|
1115
|
+
# Run specific BDD feature
|
|
1116
|
+
npx @cucumber/cucumber-js tests/features/login.feature
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
### 22. Example Test Using Utilities (`tests/specs/formTest.spec.js`)
|
|
1120
|
+
```javascript
|
|
1121
|
+
import { test, expect } from '../fixtures/base.fixture';
|
|
1122
|
+
import { FormPage } from '../pages/FormPage';
|
|
1123
|
+
import { TestDataManager } from '../utils/testData';
|
|
1124
|
+
|
|
1125
|
+
test.describe('Form Registration with Test Data', () => {
|
|
1126
|
+
let formPage;
|
|
1127
|
+
|
|
1128
|
+
test.beforeEach(async ({ page }) => {
|
|
1129
|
+
formPage = new FormPage(page);
|
|
1130
|
+
await page.goto('https://example.com/register');
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
test('Register with JSON user data', async ({ page }) => {
|
|
1134
|
+
const user = TestDataManager.getUserByUsername('standard_user');
|
|
1135
|
+
|
|
1136
|
+
await formPage.formHelper.fillInput('#firstName', user.firstName);
|
|
1137
|
+
await formPage.formHelper.fillInput('#lastName', user.lastName);
|
|
1138
|
+
await formPage.formHelper.fillInput('#email', user.email);
|
|
1139
|
+
|
|
1140
|
+
await page.click('button[type="submit"]');
|
|
1141
|
+
await expect(page).toHaveURL('**/success');
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
test('Register with CSV test scenarios', async ({ page }) => {
|
|
1145
|
+
await formPage.fillRegistrationForm('registration_valid');
|
|
1146
|
+
|
|
1147
|
+
await page.click('button[type="submit"]');
|
|
1148
|
+
await expect(page).toHaveURL('**/success');
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
test('Fill complex form with multiple field types', async ({ page }) => {
|
|
1152
|
+
const formConfig = {
|
|
1153
|
+
firstName: {
|
|
1154
|
+
type: 'text',
|
|
1155
|
+
selector: '#firstName',
|
|
1156
|
+
value: 'John'
|
|
1157
|
+
},
|
|
1158
|
+
country: {
|
|
1159
|
+
type: 'dropdown',
|
|
1160
|
+
selector: '.country-select',
|
|
1161
|
+
value: 'United States'
|
|
1162
|
+
},
|
|
1163
|
+
gender: {
|
|
1164
|
+
type: 'radio',
|
|
1165
|
+
value: 'Male'
|
|
1166
|
+
},
|
|
1167
|
+
interests: {
|
|
1168
|
+
type: 'checkbox',
|
|
1169
|
+
value: ['Sports', 'Gaming', 'Reading']
|
|
1170
|
+
},
|
|
1171
|
+
terms: {
|
|
1172
|
+
type: 'checkbox',
|
|
1173
|
+
value: 'I accept terms'
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
// Use helper to fill entire form
|
|
1178
|
+
for (const field of Object.values(formConfig)) {
|
|
1179
|
+
if (field.type === 'text') {
|
|
1180
|
+
await formPage.formHelper.fillInput(field.selector, field.value);
|
|
1181
|
+
} else if (field.type === 'dropdown') {
|
|
1182
|
+
await formPage.formHelper.selectDropdown(field.selector, field.value);
|
|
1183
|
+
} else if (field.type === 'radio') {
|
|
1184
|
+
await formPage.formHelper.selectRadioButton(field.value);
|
|
1185
|
+
} else if (field.type === 'checkbox') {
|
|
1186
|
+
if (Array.isArray(field.value)) {
|
|
1187
|
+
await formPage.formHelper.checkMultiple(field.value);
|
|
1188
|
+
} else {
|
|
1189
|
+
await formPage.formHelper.checkCheckbox(field.value);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
await page.click('button[type="submit"]');
|
|
1195
|
+
await expect(page).toHaveURL('**/success');
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
test('Verify form values after filling', async ({ page }) => {
|
|
1199
|
+
await formPage.formHelper.fillInput('#firstName', 'John');
|
|
1200
|
+
await formPage.formHelper.checkCheckbox('I accept terms');
|
|
1201
|
+
|
|
1202
|
+
const firstName = await formPage.formHelper.getInputValue('#firstName');
|
|
1203
|
+
const isTermsChecked = await formPage.formHelper.isCheckboxChecked('I accept terms');
|
|
1204
|
+
|
|
1205
|
+
expect(firstName).toBe('John');
|
|
1206
|
+
expect(isTermsChecked).toBe(true);
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
### 23. BDD Step with Test Data Integration
|
|
1212
|
+
```gherkin
|
|
1213
|
+
Feature: Registration with Test Data
|
|
1214
|
+
As a new user
|
|
1215
|
+
I want to register using predefined test data
|
|
1216
|
+
So that I can access the application
|
|
1217
|
+
|
|
1218
|
+
Scenario: Register with JSON test data
|
|
1219
|
+
Given I navigate to the registration page
|
|
1220
|
+
When I fill the form with user data "standard_user"
|
|
1221
|
+
And I accept the terms and conditions
|
|
1222
|
+
And I submit the form
|
|
1223
|
+
Then I should see the success message
|
|
1224
|
+
|
|
1225
|
+
Scenario Outline: Register with CSV test scenarios
|
|
1226
|
+
Given I navigate to the registration page
|
|
1227
|
+
When I fill first name with "<firstName>"
|
|
1228
|
+
And I fill last name with "<lastName>"
|
|
1229
|
+
And I fill email with "<email>"
|
|
1230
|
+
And I select country "<country>"
|
|
1231
|
+
And I check "<acceptTerms>"
|
|
1232
|
+
And I submit the form
|
|
1233
|
+
Then I should see the success message
|
|
1234
|
+
|
|
1235
|
+
Examples:
|
|
1236
|
+
| firstName | lastName | email | country | acceptTerms |
|
|
1237
|
+
| John | Doe | john@test.com | USA | true |
|
|
1238
|
+
| Jane | Smith | jane@test.com | Canada | true |
|
|
1239
|
+
| Pierre | Martin | pierre@test.fr | France | true |
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
## Developer Quick Start
|
|
1243
|
+
|
|
1244
|
+
### Using Test Data & Form Helpers
|
|
1245
|
+
**Step 1:** Create test data files in `tests/data/` (JSON, CSV, YAML)
|
|
1246
|
+
**Step 2:** Use `DataReader` to load test data:
|
|
1247
|
+
```javascript
|
|
1248
|
+
const users = DataReader.readJSON('users.json');
|
|
1249
|
+
const formData = await DataReader.readCSV('formData.csv');
|
|
1250
|
+
const config = DataReader.readYAML('config.yaml');
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
**Step 3:** Use `FormHelper` for complex form interactions:
|
|
1254
|
+
```javascript
|
|
1255
|
+
const formHelper = new FormHelper(page);
|
|
1256
|
+
await formHelper.fillInput('#name', 'John');
|
|
1257
|
+
await formHelper.selectDropdown('.country', 'USA');
|
|
1258
|
+
await formHelper.checkMultiple(['Option1', 'Option2']);
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
**Step 4:** Create page objects that combine both:
|
|
1262
|
+
```javascript
|
|
1263
|
+
const formPage = new FormPage(page);
|
|
1264
|
+
await formPage.fillRegistrationForm('registration_valid');
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
### Traditional Playwright Tests
|
|
1268
|
+
**Step 1:** Copy project structure from Section 4
|
|
1269
|
+
**Step 2:** Create page objects in `tests/pages/` (copy LoginPage & InventoryPage)
|
|
1270
|
+
**Step 3:** Import fixtures in your tests: `import { test, expect } from '../fixtures/base.fixture'`
|
|
1271
|
+
**Step 4:** Use fixtures in tests: `test('my test', async ({ loginPage, authenticatedUser }) => { ... })`
|
|
1272
|
+
**Step 5:** Run: `npm test`
|
|
1273
|
+
|
|
1274
|
+
### BDD Scenarios
|
|
1275
|
+
**Step 1:** Create `.feature` files in `tests/features/` with Gherkin scenarios
|
|
1276
|
+
**Step 2:** Create step definitions in `tests/steps/` (copy loginSteps & shoppingSteps)
|
|
1277
|
+
**Step 3:** Map steps to page objects (reuse LoginPage, InventoryPage)
|
|
1278
|
+
**Step 4:** Configure `cucumber.js` for parallel execution
|
|
1279
|
+
**Step 5:** Run: `npm run test:bdd`
|
|
99
1280
|
|
|
100
1281
|
## Verification Checklist
|
|
101
|
-
✅
|
|
102
|
-
✅
|
|
103
|
-
✅
|
|
104
|
-
✅
|
|
105
|
-
✅
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1282
|
+
✅ Chromium browser installed (`npx playwright install chromium`)
|
|
1283
|
+
✅ TypeScript configured (`typescript @types/node` installed + `tsconfig.json` created)
|
|
1284
|
+
✅ All dependencies installed (`npm install` + `@cucumber/cucumber` + `csv-parser`, `js-yaml`, `@types/js-yaml`)
|
|
1285
|
+
✅ TypeScript paths configured in `tsconfig.json` (@pages/*, @utils/*, @fixtures/*, @data/*)
|
|
1286
|
+
✅ Playwright config in TypeScript (`playwright.config.ts` with latest imports)
|
|
1287
|
+
✅ Folder structure created (all `.ts` files: `tests/pages/`, `tests/fixtures/`, `tests/specs/`, `tests/features/`, `tests/steps/`, `tests/utils/`, `tests/data/`)
|
|
1288
|
+
✅ Base fixtures created with TypeScript typing (`base.fixture.ts`)
|
|
1289
|
+
✅ Cucumber configuration created (`cucumber.js` with ts-node support)
|
|
1290
|
+
✅ Page Object Model classes in TypeScript with type safety
|
|
1291
|
+
✅ Step definitions in TypeScript with proper typing
|
|
1292
|
+
✅ Feature files created (login.feature, shopping.feature)
|
|
1293
|
+
✅ Utilities created in TypeScript (dataReader.ts, formHelper.ts, testData.ts with interfaces)
|
|
1294
|
+
✅ Test data files created (users.json, formData.csv, config.yaml)
|
|
1295
|
+
✅ Enhanced page objects in TypeScript (FormPage.ts with utilities integrated)
|
|
1296
|
+
✅ Modern ES module imports throughout (import/export)
|
|
1297
|
+
✅ Parallel execution enabled (`fullyParallel: true` & Cucumber parallel: 4)
|
|
1298
|
+
✅ Tests execute in parallel against single Chromium browser
|
|
1299
|
+
✅ Both Playwright & BDD tests configured with full TypeScript support
|
|
1300
|
+
|
|
1301
|
+
## Best Practices for Scalability & TypeScript
|
|
1302
|
+
✅ **Use Fixtures** - Pre-set up authenticated users, page objects (base.fixture.ts with types)
|
|
1303
|
+
✅ **Page Object Model** - Encapsulate selectors & actions in TypeScript classes
|
|
1304
|
+
✅ **Type Safety** - Full TypeScript typing for all utilities, pages, and tests
|
|
1305
|
+
✅ **Test Data Management** - Use JSON, CSV, YAML with DataReader (fully typed)
|
|
1306
|
+
✅ **Form Helpers** - Reusable FormHelper for all form interactions (typed interfaces)
|
|
1307
|
+
✅ **BDD for Non-Technical Stakeholders** - Write scenarios in Gherkin (Feature files)
|
|
1308
|
+
✅ **Parallel Execution** - Tests & scenarios run simultaneously (4 workers by default)
|
|
1309
|
+
✅ **Minimal Developer Effort** - Write only test logic, reuse fixtures, pages, steps & utilities
|
|
1310
|
+
✅ **Chromium Only** - Single browser reduces flakiness & speeds up execution
|
|
1311
|
+
✅ **Retry Failed Tests** - Automatic retries improve reliability
|
|
1312
|
+
✅ **Capture Failures** - Screenshots & videos on failures for debugging
|
|
1313
|
+
✅ **Dual Approach** - Mix traditional tests & BDD scenarios based on needs
|
|
1314
|
+
✅ **Module Path Aliases** - Use @pages/, @utils/, @fixtures/, @data/ for clean imports
|
|
1315
|
+
✅ **Latest Playwright** - Uses latest Playwright APIs with modern module syntax
|
|
1316
|
+
✅ **Parameterized Tests** - Use CSV data for scenario outlines (data-driven testing)
|
|
1317
|
+
✅ **Form Helpers** - Reusable FormHelper for all form interactions (text, dropdown, radio, checkbox)
|
|
1318
|
+
✅ **BDD for Non-Technical Stakeholders** - Write scenarios in Gherkin (Feature files)
|
|
1319
|
+
✅ **Parallel Execution** - Tests & scenarios run simultaneously (4 workers by default)
|
|
1320
|
+
✅ **Minimal Developer Effort** - Write only test logic, reuse fixtures, pages, steps & utilities
|
|
1321
|
+
✅ **Chromium Only** - Single browser reduces flakiness & speeds up execution
|
|
1322
|
+
✅ **Retry Failed Tests** - Automatic retries improve reliability
|
|
1323
|
+
✅ **Capture Failures** - Screenshots & videos on failures for debugging
|
|
1324
|
+
✅ **Dual Approach** - Mix traditional tests & BDD scenarios based on needs
|
|
1325
|
+
✅ **Parameterized Tests** - Use CSV data for scenario outlines (data-driven testing)
|
|
1326
|
+
✅ **Full TypeScript Support** - All code written in TypeScript with strict mode enabled
|
|
1327
|
+
✅ **Path Aliases** - Clean imports using @pages/, @utils/, @fixtures/, @data/ prefixes
|
|
1328
|
+
|
|
1329
|
+
## How to Use Test Data & Utilities (TypeScript)
|
|
1330
|
+
|
|
1331
|
+
### 1. Loading Test Data
|
|
1332
|
+
```typescript
|
|
1333
|
+
import { TestDataManager } from '@utils/testData';
|
|
1334
|
+
|
|
1335
|
+
// Get user by username (fully typed)
|
|
1336
|
+
const user = TestDataManager.getUserByUsername('standard_user');
|
|
1337
|
+
|
|
1338
|
+
// Get form data by scenario
|
|
1339
|
+
const formData = await TestDataManager.getFormDataByScenario('registration_valid');
|
|
1340
|
+
|
|
1341
|
+
// Load all config with type safety
|
|
1342
|
+
const config = TestDataManager.loadConfig();
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
### 2. Form Interactions with Type Safety
|
|
1346
|
+
```typescript
|
|
1347
|
+
import { FormHelper } from '@utils/formHelper';
|
|
1348
|
+
import { Page } from '@playwright/test';
|
|
1349
|
+
|
|
1350
|
+
const formHelper = new FormHelper(page);
|
|
1351
|
+
|
|
1352
|
+
// Text input
|
|
1353
|
+
await formHelper.fillInput('#firstName', 'John');
|
|
1354
|
+
|
|
1355
|
+
// Dropdown (non-select)
|
|
1356
|
+
await formHelper.selectDropdown('.country-dropdown', 'USA');
|
|
1357
|
+
|
|
1358
|
+
// Radio button
|
|
1359
|
+
await formHelper.selectRadioButton('Male');
|
|
1360
|
+
|
|
1361
|
+
// Single checkbox
|
|
1362
|
+
await formHelper.checkCheckbox('I accept terms');
|
|
1363
|
+
|
|
1364
|
+
// Multiple checkboxes
|
|
1365
|
+
await formHelper.checkMultiple(['Option1', 'Option2', 'Option3']);
|
|
1366
|
+
|
|
1367
|
+
// Get and verify values (fully typed)
|
|
1368
|
+
const firstName = await formHelper.getInputValue('#firstName');
|
|
1369
|
+
const isChecked = await formHelper.isCheckboxChecked('I accept terms');
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
### 3. Data-Driven Tests in TypeScript
|
|
1373
|
+
```typescript
|
|
1374
|
+
import { test, expect } from '@fixtures/base.fixture';
|
|
1375
|
+
import { FormPage } from '@pages/FormPage';
|
|
1376
|
+
|
|
1377
|
+
test.describe('Data-Driven Form Tests', () => {
|
|
1378
|
+
const testCases = [
|
|
1379
|
+
{ scenario: 'registration_valid' as const },
|
|
1380
|
+
{ scenario: 'registration_minimal' as const },
|
|
1381
|
+
{ scenario: 'registration_eu' as const }
|
|
1382
|
+
];
|
|
1383
|
+
|
|
1384
|
+
testCases.forEach((testCase) => {
|
|
1385
|
+
test(`Register with ${testCase.scenario}`, async ({ page, formPage }: any) => {
|
|
1386
|
+
await formPage.fillRegistrationForm(testCase.scenario);
|
|
1387
|
+
// Assert...
|
|
1388
|
+
});
|
|
1389
|
+
});
|
|
1390
|
+
});
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
### 4. Using Module Path Aliases
|
|
1394
|
+
```typescript
|
|
1395
|
+
// Clean imports with path aliases
|
|
1396
|
+
import { LoginPage } from '@pages/LoginPage';
|
|
1397
|
+
import { FormHelper } from '@utils/formHelper';
|
|
1398
|
+
import { DataReader } from '@utils/dataReader';
|
|
1399
|
+
import { test, expect } from '@fixtures/base.fixture';
|
|
1400
|
+
|
|
1401
|
+
// Old way (avoid)
|
|
1402
|
+
// import { LoginPage } from '../pages/LoginPage';
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
## When to Use Traditional vs BDD
|
|
1406
|
+
**Use Traditional Tests (`npm test`)** when:
|
|
1407
|
+
- Writing complex technical validations
|
|
1408
|
+
- Need direct access to page objects with full typing
|
|
1409
|
+
- Testing internal APIs or utilities
|
|
1410
|
+
- Data-driven test variations
|
|
1411
|
+
- Using TypeScript for type safety
|
|
1412
|
+
|
|
1413
|
+
**Use BDD (`npm run test:bdd`)** when:
|
|
1414
|
+
- Collaborating with non-technical stakeholders
|
|
1415
|
+
- Business logic needs clear documentation
|
|
1416
|
+
- Multiple teams working on same scenarios
|
|
1417
|
+
- Acceptance criteria need to be testable
|
|
1418
|
+
|
|
1419
|
+
**Note:** This setup uses Chromium only with parallel execution, full TypeScript support with strict mode, and modern ES module imports. Both Playwright & BDD tests can run simultaneously with complete type safety. All utilities and page objects are fully typed. Module path aliases (@pages/, @utils/, etc.) provide clean imports. Latest Playwright features and APIs are used throughout. If you see "Missing script: start" error, add the `start` script to your package.json before running tests.
|