agents-cli-automation 1.0.18 → 1.0.19
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/package.json +1 -1
- package/src/templates/playwright-agent.md +302 -1317
|
@@ -1,1419 +1,404 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Playwright Test Automation Framework
|
|
2
|
+
|
|
3
|
+
**Name:** Playwright Agent - TypeScript Test Automation Framework
|
|
4
|
+
|
|
5
|
+
**Description:** A comprehensive test automation framework built with Playwright and TypeScript for testing UI, API, and database layers. This framework demonstrates best practices for organizing test code, managing test data, implementing step definitions with Playwright BDD, and maintaining database state during testing. It provides examples of test-harness clients for database access, API calls, correlation tracking, and fixture management.
|
|
4
6
|
|
|
5
7
|
---
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
9
|
+
## Folder Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
automation-framework/
|
|
13
|
+
├─ package.json
|
|
14
|
+
├─ tsconfig.json
|
|
15
|
+
├─ playwright.config.ts
|
|
16
|
+
├─ .env
|
|
17
|
+
│
|
|
18
|
+
├─ configs/
|
|
19
|
+
│ ├─ projects/
|
|
20
|
+
│ │ ├─ ui.project.ts
|
|
21
|
+
│ │ ├─ api.project.ts
|
|
22
|
+
│ │ ├─ db.project.ts
|
|
23
|
+
│ │ └─ integrated.project.ts
|
|
24
|
+
│ ├─ environments/
|
|
25
|
+
│ └─ global-setup.ts
|
|
26
|
+
│
|
|
27
|
+
├─ scripts/
|
|
28
|
+
│ ├─ seed-db.ts
|
|
29
|
+
│ ├─ reset-db.ts
|
|
30
|
+
│ └─ run-wrapper.ts
|
|
31
|
+
│
|
|
32
|
+
├─ src/
|
|
33
|
+
│ ├─ core/
|
|
34
|
+
│ │ ├─ test-harness/
|
|
35
|
+
│ │ │ ├─ dbClient.ts
|
|
36
|
+
│ │ │ └─ wrapperClient.ts
|
|
37
|
+
│ │ ├─ fixtures/
|
|
38
|
+
│ │ │ └─ context.fixture.ts
|
|
39
|
+
│ │ └─ utils/
|
|
40
|
+
│ │ ├─ logger.ts
|
|
41
|
+
│ │ └─ correlation.ts
|
|
42
|
+
│ │
|
|
43
|
+
│ ├─ modules/
|
|
44
|
+
│ │ ├─ scheduling-group/
|
|
45
|
+
│ │ │ ├─ features/
|
|
46
|
+
│ │ │ │ └─ create-scheduling-group.feature
|
|
47
|
+
│ │ │ ├─ steps/
|
|
48
|
+
│ │ │ │ └─ createSchedulingGroup.steps.ts
|
|
49
|
+
│ │ │ ├─ read-models/
|
|
50
|
+
│ │ │ │ └─ schedule.read.ts
|
|
51
|
+
│ │ │ ├─ queries/
|
|
52
|
+
│ │ │ │ └─ schedule.mutations.ts
|
|
53
|
+
│ │ │ ├─ contracts/
|
|
54
|
+
│ │ │ │ └─ schedule.wrapper.contract.ts
|
|
55
|
+
│ │ │ └─ test-data/
|
|
56
|
+
│ │ │ └─ schedule.seed.ts
|
|
57
|
+
│ │ └─ <future-module>/
|
|
58
|
+
│ │
|
|
59
|
+
│ └─ shared/
|
|
60
|
+
│ ├─ constants/
|
|
61
|
+
│ ├─ types/
|
|
62
|
+
│ └─ data-factory/
|
|
63
|
+
│
|
|
64
|
+
├─ reports/
|
|
65
|
+
└─ README.md
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Environment Configuration
|
|
69
|
+
|
|
70
|
+
### .env
|
|
39
71
|
|
|
40
|
-
### 2. Install BDD & Data Handling Dependencies
|
|
41
|
-
For Playwright BDD support and test data files:
|
|
42
72
|
```bash
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
# Database
|
|
74
|
+
DB_HOST=localhost
|
|
75
|
+
DB_PORT=1433
|
|
76
|
+
DB_USER=sa
|
|
77
|
+
DB_PASSWORD=YourStrong@Passw0rd
|
|
78
|
+
DB_NAME=AutomationTestDB
|
|
45
79
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```json
|
|
49
|
-
{
|
|
50
|
-
"scripts": {
|
|
51
|
-
"start": "node server.js",
|
|
52
|
-
"test": "playwright test",
|
|
53
|
-
"test:headed": "playwright test --headed",
|
|
54
|
-
"test:debug": "playwright test --debug",
|
|
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"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
```
|
|
80
|
+
# Wrapper / API
|
|
81
|
+
WRAPPER_URL=http://localhost:4000/test-wrapper
|
|
61
82
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
}
|
|
83
|
+
# Test Config
|
|
84
|
+
NODE_ENV=test
|
|
91
85
|
```
|
|
92
86
|
|
|
93
|
-
|
|
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
|
-
```
|
|
87
|
+
## Core Components
|
|
126
88
|
|
|
127
|
-
###
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
89
|
+
### DB Client – `core/test-harness/dbClient.ts`
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { ConnectionPool } from "mssql";
|
|
93
|
+
import dotenv from "dotenv";
|
|
94
|
+
dotenv.config();
|
|
95
|
+
|
|
96
|
+
const pool = new ConnectionPool({
|
|
97
|
+
user: process.env.DB_USER,
|
|
98
|
+
password: process.env.DB_PASSWORD,
|
|
99
|
+
server: process.env.DB_HOST,
|
|
100
|
+
database: process.env.DB_NAME,
|
|
101
|
+
options: { encrypt: true, trustServerCertificate: true }
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const dbClient = {
|
|
105
|
+
query: async (query: string, params?: Record<string, any>) => {
|
|
106
|
+
const connection = await pool.connect();
|
|
107
|
+
const request = connection.request();
|
|
108
|
+
if (params) {
|
|
109
|
+
for (const key in params) {
|
|
110
|
+
request.input(key, params[key]);
|
|
111
|
+
}
|
|
138
112
|
}
|
|
113
|
+
const result = await request.query(query);
|
|
114
|
+
return result.recordset;
|
|
139
115
|
}
|
|
140
116
|
};
|
|
141
117
|
```
|
|
142
118
|
|
|
143
|
-
###
|
|
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
|
-
},
|
|
163
|
-
webServer: {
|
|
164
|
-
command: 'npm start',
|
|
165
|
-
url: 'http://localhost:3000',
|
|
166
|
-
reuseExistingServer: !process.env.CI,
|
|
167
|
-
},
|
|
168
|
-
reporter: [
|
|
169
|
-
['html'],
|
|
170
|
-
['json', { outputFile: 'test-results/results.json' }],
|
|
171
|
-
],
|
|
172
|
-
});
|
|
173
|
-
```
|
|
119
|
+
### Wrapper Client / API Layer – `core/test-harness/wrapperClient.ts`
|
|
174
120
|
|
|
175
|
-
### 8. Data Reader Utility (`tests/utils/dataReader.ts`) - TypeScript
|
|
176
121
|
```typescript
|
|
177
|
-
import
|
|
178
|
-
import
|
|
179
|
-
import csv from 'csv-parser';
|
|
180
|
-
import yaml from 'js-yaml';
|
|
122
|
+
import axios from "axios";
|
|
123
|
+
import { utils } from "../utils/correlation";
|
|
181
124
|
|
|
182
|
-
const
|
|
125
|
+
export const wrapperClient = {
|
|
126
|
+
createSchedulingGroup: async (payload: any, user: any) => {
|
|
127
|
+
const correlationId = utils.getCorrelationId();
|
|
183
128
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
129
|
+
const headers = {
|
|
130
|
+
"x-correlation-id": correlationId,
|
|
131
|
+
"x-test-user-id": user.id
|
|
132
|
+
};
|
|
187
133
|
|
|
188
|
-
|
|
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
|
-
}
|
|
134
|
+
utils.log("Calling wrapper API", { payload, user });
|
|
195
135
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
136
|
+
const response = await axios.post(
|
|
137
|
+
`${process.env.WRAPPER_URL}/scheduling-groups`,
|
|
138
|
+
payload,
|
|
139
|
+
{ headers }
|
|
140
|
+
);
|
|
202
141
|
|
|
203
|
-
|
|
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
|
-
}
|
|
142
|
+
utils.log("Wrapper API response", response.data);
|
|
215
143
|
|
|
216
|
-
|
|
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;
|
|
144
|
+
return { ...response.data, correlationId };
|
|
226
145
|
}
|
|
227
|
-
}
|
|
146
|
+
};
|
|
228
147
|
```
|
|
229
148
|
|
|
230
|
-
###
|
|
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
|
-
```
|
|
149
|
+
### Utilities – `core/utils/correlation.ts`
|
|
343
150
|
|
|
344
|
-
### 10. Test Data Manager (`tests/utils/testData.ts`) - TypeScript
|
|
345
151
|
```typescript
|
|
346
|
-
import {
|
|
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
|
-
}
|
|
152
|
+
import { v4 as uuidv4 } from "uuid";
|
|
390
153
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return await DataReader.readCSV('formData.csv') as unknown as FormData[];
|
|
394
|
-
}
|
|
154
|
+
export const utils = {
|
|
155
|
+
getCorrelationId: (): string => uuidv4(),
|
|
395
156
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
}
|
|
157
|
+
normalizeTimestamps: (record: any) => {
|
|
158
|
+
if (record.createdAt) record.createdAt = "<timestamp>";
|
|
159
|
+
if (record.updatedAt) record.updatedAt = "<timestamp>";
|
|
160
|
+
return record;
|
|
161
|
+
},
|
|
406
162
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const data = await this.loadFormData();
|
|
410
|
-
return DataReader.getRecordByKey(data, 'scenario' as keyof FormData, scenarioName);
|
|
163
|
+
log: (message: string, data?: any) => {
|
|
164
|
+
console.log(`[${new Date().toISOString()}] ${message}`, data || "");
|
|
411
165
|
}
|
|
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
|
|
166
|
+
};
|
|
447
167
|
```
|
|
448
168
|
|
|
449
|
-
|
|
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'
|
|
169
|
+
### Fixtures – `core/fixtures/context.fixture.ts`
|
|
471
170
|
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
}
|
|
171
|
+
```typescript
|
|
172
|
+
export const contextProvider = {
|
|
173
|
+
getCurrentUser: async () => {
|
|
174
|
+
return { id: "default-system-admin", role: "System Admin" };
|
|
175
|
+
},
|
|
486
176
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
}
|
|
177
|
+
createTestContext: () => {
|
|
178
|
+
return {
|
|
179
|
+
user: null,
|
|
180
|
+
payload: null,
|
|
181
|
+
response: null,
|
|
182
|
+
correlationId: null
|
|
519
183
|
};
|
|
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
184
|
}
|
|
533
|
-
}
|
|
185
|
+
};
|
|
534
186
|
```
|
|
535
187
|
|
|
536
|
-
|
|
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
|
-
}
|
|
188
|
+
## Test Data & Database
|
|
552
189
|
|
|
553
|
-
|
|
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
|
-
```
|
|
190
|
+
### Test Data Builder – `modules/scheduling-group/test-data/schedule.seed.ts`
|
|
602
191
|
|
|
603
|
-
### 12. Fixtures Setup (`tests/fixtures/base.fixture.ts`) - TypeScript
|
|
604
192
|
```typescript
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
formPage: FormPage;
|
|
614
|
-
authenticatedUser: { page: any; loginPage: LoginPage };
|
|
193
|
+
export const schedulingGroupBuilder = {
|
|
194
|
+
createValid: () => ({
|
|
195
|
+
name: `MorningNews_${Date.now()}`,
|
|
196
|
+
areaId: 101,
|
|
197
|
+
allocationsMenu: true,
|
|
198
|
+
notes: "Used for weekday planning",
|
|
199
|
+
teams: [201, 202]
|
|
200
|
+
})
|
|
615
201
|
};
|
|
202
|
+
```
|
|
616
203
|
|
|
617
|
-
|
|
618
|
-
loginPage: async ({ page }, use) => {
|
|
619
|
-
const loginPage = new LoginPage(page);
|
|
620
|
-
await use(loginPage);
|
|
621
|
-
},
|
|
204
|
+
### Read-Models – `modules/scheduling-group/read-models/schedule.read.ts`
|
|
622
205
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
await use(inventoryPage);
|
|
626
|
-
},
|
|
206
|
+
```typescript
|
|
207
|
+
import { dbClient } from "../../core/test-harness/dbClient";
|
|
627
208
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
await
|
|
209
|
+
export const schedulingGroupRead = {
|
|
210
|
+
getByName: async (name: string) => {
|
|
211
|
+
const result = await dbClient.query(`SELECT * FROM SchedulingGroup WHERE Name = @name`, { name });
|
|
212
|
+
return result[0] || null;
|
|
631
213
|
},
|
|
632
214
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
await loginPage.login('standard_user', 'secret_sauce');
|
|
636
|
-
await use({ page, loginPage });
|
|
215
|
+
getTeamsByGroupId: async (groupId: number) => {
|
|
216
|
+
return dbClient.query(`SELECT * FROM SchedulingGroupTeam WHERE SchedulingGroupId = @groupId`, { groupId });
|
|
637
217
|
},
|
|
638
|
-
});
|
|
639
|
-
|
|
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
218
|
|
|
674
|
-
async
|
|
675
|
-
|
|
219
|
+
getHistoryByGroupId: async (groupId: number) => {
|
|
220
|
+
return dbClient.query(
|
|
221
|
+
`SELECT * FROM SchedulingGroupHistory WHERE SchedulingGroupId = @groupId ORDER BY LastAmendedDate DESC`,
|
|
222
|
+
{ groupId }
|
|
223
|
+
);
|
|
676
224
|
}
|
|
677
|
-
|
|
678
|
-
async isVisible(selector: string): Promise<boolean> {
|
|
679
|
-
return await this.page.locator(selector).isVisible();
|
|
680
|
-
}
|
|
681
|
-
}
|
|
225
|
+
};
|
|
682
226
|
```
|
|
683
227
|
|
|
684
|
-
###
|
|
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
|
-
```
|
|
228
|
+
### DB Stub / Invariants – `modules/scheduling-group/queries/schedule.mutations.ts`
|
|
710
229
|
|
|
711
|
-
### 15. Inventory Page (`tests/pages/InventoryPage.ts`) - TypeScript
|
|
712
230
|
```typescript
|
|
713
|
-
import {
|
|
714
|
-
|
|
715
|
-
export
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
231
|
+
import { dbClient } from "../../core/test-harness/dbClient";
|
|
232
|
+
|
|
233
|
+
export const schedulingGroupMutations = {
|
|
234
|
+
createSchedulingGroupStub: async (payload: any, userId: string) => {
|
|
235
|
+
const result = await dbClient.query(
|
|
236
|
+
`INSERT INTO SchedulingGroup (Name, AreaId, AllocationsMenu, Notes, LastAmendedBy, LastAmendedDate)
|
|
237
|
+
VALUES (@name, @areaId, @allocationsMenu, @notes, @userId, GETDATE());
|
|
238
|
+
SELECT SCOPE_IDENTITY() AS Id;`,
|
|
239
|
+
{
|
|
240
|
+
name: payload.name,
|
|
241
|
+
areaId: payload.areaId,
|
|
242
|
+
allocationsMenu: payload.allocationsMenu,
|
|
243
|
+
notes: payload.notes,
|
|
244
|
+
userId
|
|
245
|
+
}
|
|
246
|
+
);
|
|
720
247
|
|
|
721
|
-
|
|
722
|
-
await this.click(this.addToCartBtn);
|
|
723
|
-
}
|
|
248
|
+
const groupId = result[0].Id;
|
|
724
249
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
250
|
+
for (const teamId of payload.teams) {
|
|
251
|
+
await dbClient.query(
|
|
252
|
+
`INSERT INTO SchedulingGroupTeam (SchedulingGroupId, TeamId) VALUES (@groupId, @teamId)`,
|
|
253
|
+
{ groupId, teamId }
|
|
254
|
+
);
|
|
255
|
+
}
|
|
728
256
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
257
|
+
await dbClient.query(
|
|
258
|
+
`INSERT INTO SchedulingGroupHistory (SchedulingGroupId, Name, AreaId, AllocationsMenu, Notes, LastAmendedBy, LastAmendedDate)
|
|
259
|
+
VALUES (@groupId, @name, @areaId, @allocationsMenu, @notes, @userId, GETDATE())`,
|
|
260
|
+
{
|
|
261
|
+
groupId,
|
|
262
|
+
name: payload.name,
|
|
263
|
+
areaId: payload.areaId,
|
|
264
|
+
allocationsMenu: payload.allocationsMenu,
|
|
265
|
+
notes: payload.notes,
|
|
266
|
+
userId
|
|
267
|
+
}
|
|
268
|
+
);
|
|
733
269
|
|
|
734
|
-
|
|
735
|
-
await this.click(this.menuButton);
|
|
736
|
-
await this.click(this.logoutLink);
|
|
737
|
-
await this.page.waitForURL('https://www.saucedemo.com/');
|
|
270
|
+
return groupId;
|
|
738
271
|
}
|
|
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
|
-
});
|
|
763
|
-
});
|
|
764
|
-
```
|
|
765
|
-
|
|
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
|
-
});
|
|
272
|
+
};
|
|
801
273
|
```
|
|
802
274
|
|
|
803
|
-
|
|
804
|
-
```bash
|
|
805
|
-
# Run all tests in parallel (4 workers)
|
|
806
|
-
npm test
|
|
807
|
-
|
|
808
|
-
# Run specific test file
|
|
809
|
-
npm test tests/specs/saucedemo.spec.ts
|
|
810
|
-
|
|
811
|
-
# Run tests with UI (single worker)
|
|
812
|
-
npm run test:headed
|
|
813
|
-
|
|
814
|
-
# Debug mode
|
|
815
|
-
npm run test:debug
|
|
275
|
+
## Test Implementation
|
|
816
276
|
|
|
817
|
-
|
|
818
|
-
npm test -- --workers=2
|
|
277
|
+
### Step Definitions – `modules/scheduling-group/steps/createSchedulingGroup.steps.ts`
|
|
819
278
|
|
|
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
279
|
```typescript
|
|
860
|
-
import { Given, When, Then
|
|
861
|
-
import {
|
|
862
|
-
import {
|
|
863
|
-
import {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
});
|
|
280
|
+
import { Given, When, Then } from "@cucumber/cucumber";
|
|
281
|
+
import { wrapperClient } from "../../core/test-harness/wrapperClient";
|
|
282
|
+
import { schedulingGroupMutations } from "../queries/schedule.mutations";
|
|
283
|
+
import { schedulingGroupRead } from "../read-models/schedule.read";
|
|
284
|
+
import { schedulingGroupBuilder } from "../test-data/schedule.seed";
|
|
285
|
+
import { contextProvider } from "../../core/fixtures/context.fixture";
|
|
286
|
+
import { utils } from "../../core/utils/correlation";
|
|
287
|
+
import { expect } from "@playwright/test";
|
|
881
288
|
|
|
882
|
-
Given(
|
|
883
|
-
|
|
289
|
+
Given("an Area Admin user is available", async function () {
|
|
290
|
+
this.context = contextProvider.createTestContext();
|
|
291
|
+
this.context.user = await contextProvider.getCurrentUser();
|
|
884
292
|
});
|
|
885
293
|
|
|
886
|
-
|
|
887
|
-
|
|
294
|
+
Given("a valid Scheduling Group payload is prepared", async function () {
|
|
295
|
+
this.context.payload = schedulingGroupBuilder.createValid();
|
|
296
|
+
utils.log("Payload prepared", this.context.payload);
|
|
888
297
|
});
|
|
889
298
|
|
|
890
|
-
|
|
891
|
-
await
|
|
299
|
+
When("the user creates the Scheduling Group via DB stub", async function () {
|
|
300
|
+
this.context.groupId = await schedulingGroupMutations.createSchedulingGroupStub(
|
|
301
|
+
this.context.payload,
|
|
302
|
+
this.context.user.id
|
|
303
|
+
);
|
|
892
304
|
});
|
|
893
305
|
|
|
894
|
-
Then(
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
throw new Error(`Expected title to contain "${title}" but got "${pageTitle}"`);
|
|
898
|
-
}
|
|
899
|
-
});
|
|
306
|
+
Then("the Scheduling Group should exist in the database with correct invariants", async function () {
|
|
307
|
+
const record = await schedulingGroupRead.getByName(this.context.payload.name);
|
|
308
|
+
const normalized = utils.normalizeTimestamps(record);
|
|
900
309
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
310
|
+
expect(normalized).not.toBeNull();
|
|
311
|
+
expect(normalized.areaId).toBe(this.context.payload.areaId);
|
|
312
|
+
expect(normalized.allocationsMenu).toBe(this.context.payload.allocationsMenu);
|
|
313
|
+
expect(normalized.notes).toBe(this.context.payload.notes);
|
|
905
314
|
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
});
|
|
315
|
+
const teams = await schedulingGroupRead.getTeamsByGroupId(this.context.groupId);
|
|
316
|
+
expect(teams.map(t => t.TeamId)).toEqual(this.context.payload.teams);
|
|
916
317
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
Then('I should be back on the login page', async function() {
|
|
922
|
-
await page.waitForURL('https://www.saucedemo.com/');
|
|
318
|
+
const history = await schedulingGroupRead.getHistoryByGroupId(this.context.groupId);
|
|
319
|
+
expect(history.length).toBeGreaterThan(0);
|
|
320
|
+
expect(history[0].LastAmendedBy).toBe(this.context.user.id);
|
|
923
321
|
});
|
|
924
322
|
```
|
|
925
|
-
});
|
|
926
323
|
|
|
927
|
-
|
|
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
|
-
```
|
|
324
|
+
### Feature File – `modules/scheduling-group/features/create-scheduling-group.feature`
|
|
933
325
|
|
|
934
|
-
### 17. BDD Feature File (`tests/features/login.feature`)
|
|
935
326
|
```gherkin
|
|
936
|
-
Feature:
|
|
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
|
|
327
|
+
Feature: Create Scheduling Group
|
|
1108
328
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
});
|
|
329
|
+
Scenario: Area Admin creates a new Scheduling Group
|
|
330
|
+
Given an Area Admin user is available
|
|
331
|
+
And a valid Scheduling Group payload is prepared
|
|
332
|
+
When the user creates the Scheduling Group via DB stub
|
|
333
|
+
Then the Scheduling Group should exist in the database with correct invariants
|
|
1209
334
|
```
|
|
1210
335
|
|
|
1211
|
-
|
|
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
|
-
```
|
|
336
|
+
## Database Scripts
|
|
1241
337
|
|
|
1242
|
-
|
|
338
|
+
### Seed Script – `scripts/seed-db.ts`
|
|
1243
339
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
340
|
+
```typescript
|
|
341
|
+
import { dbClient } from "../src/core/test-harness/dbClient";
|
|
342
|
+
|
|
343
|
+
async function seed() {
|
|
344
|
+
await dbClient.query(`INSERT INTO Area(Id, Name) VALUES (101, 'London')`);
|
|
345
|
+
await dbClient.query(`
|
|
346
|
+
INSERT INTO SchedulingTeam(Id, Name, AreaId)
|
|
347
|
+
VALUES (201,'TeamA',101), (202,'TeamB',101)
|
|
348
|
+
`);
|
|
349
|
+
console.log("DB seeded successfully");
|
|
350
|
+
}
|
|
1252
351
|
|
|
1253
|
-
|
|
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']);
|
|
352
|
+
seed();
|
|
1259
353
|
```
|
|
1260
354
|
|
|
1261
|
-
|
|
1262
|
-
```javascript
|
|
1263
|
-
const formPage = new FormPage(page);
|
|
1264
|
-
await formPage.fillRegistrationForm('registration_valid');
|
|
1265
|
-
```
|
|
355
|
+
### Reset Script – `scripts/reset-db.ts`
|
|
1266
356
|
|
|
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`
|
|
1280
|
-
|
|
1281
|
-
## Verification Checklist
|
|
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
357
|
```typescript
|
|
1333
|
-
import {
|
|
1334
|
-
|
|
1335
|
-
// Get user by username (fully typed)
|
|
1336
|
-
const user = TestDataManager.getUserByUsername('standard_user');
|
|
358
|
+
import { dbClient } from "../src/core/test-harness/dbClient";
|
|
1337
359
|
|
|
1338
|
-
|
|
1339
|
-
|
|
360
|
+
async function reset() {
|
|
361
|
+
await dbClient.query(`TRUNCATE TABLE SchedulingGroup`);
|
|
362
|
+
await dbClient.query(`TRUNCATE TABLE SchedulingGroupTeam`);
|
|
363
|
+
await dbClient.query(`TRUNCATE TABLE SchedulingGroupHistory`);
|
|
364
|
+
console.log("DB reset successfully");
|
|
365
|
+
}
|
|
1340
366
|
|
|
1341
|
-
|
|
1342
|
-
const config = TestDataManager.loadConfig();
|
|
367
|
+
reset();
|
|
1343
368
|
```
|
|
1344
369
|
|
|
1345
|
-
|
|
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');
|
|
370
|
+
## Package Configuration
|
|
1357
371
|
|
|
1358
|
-
|
|
1359
|
-
await formHelper.selectRadioButton('Male');
|
|
372
|
+
### package.json Scripts
|
|
1360
373
|
|
|
1361
|
-
|
|
1362
|
-
|
|
374
|
+
```json
|
|
375
|
+
{
|
|
376
|
+
"scripts": {
|
|
377
|
+
"seed-db": "ts-node scripts/seed-db.ts",
|
|
378
|
+
"reset-db": "ts-node scripts/reset-db.ts",
|
|
379
|
+
"apitest": "playwright test --project=api",
|
|
380
|
+
"uitest": "playwright test --project=ui",
|
|
381
|
+
"test": "playwright test --project=integrated"
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
1363
385
|
|
|
1364
|
-
|
|
1365
|
-
await formHelper.checkMultiple(['Option1', 'Option2', 'Option3']);
|
|
386
|
+
## Running Tests
|
|
1366
387
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
```
|
|
388
|
+
1. **Reset the database:**
|
|
389
|
+
```bash
|
|
390
|
+
npm run reset-db
|
|
391
|
+
```
|
|
1371
392
|
|
|
1372
|
-
|
|
1373
|
-
```
|
|
1374
|
-
|
|
1375
|
-
|
|
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
|
-
```
|
|
393
|
+
2. **Seed the database:**
|
|
394
|
+
```bash
|
|
395
|
+
npm run seed-db
|
|
396
|
+
```
|
|
1392
397
|
|
|
1393
|
-
|
|
1394
|
-
```
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
-
```
|
|
398
|
+
3. **Run API/DB tests:**
|
|
399
|
+
```bash
|
|
400
|
+
npm run apitest
|
|
401
|
+
```
|
|
1404
402
|
|
|
1405
|
-
|
|
1406
|
-
|
|
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.
|
|
403
|
+
4. **View reports and logs:**
|
|
404
|
+
Check the `reports/` directory for HTML reports and detailed logs.
|