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.
@@ -1,1419 +1,404 @@
1
- name: Create Playwright Framework - UI Testing
2
- description: Creates a production-ready Playwright UI automation framework with all dependencies, fixtures, and test configurations properly set up.
3
- argument-hint: "framework requirements, e.g., 'JavaScript with UI tests'"
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
- # Playwright UI Testing Framework Setup
8
-
9
- This agent creates a production-ready Playwright framework optimized for UI automation.
10
-
11
- ## Capabilities
12
- - Playwright setup (JS / TS) - Chromium only with latest module syntax
13
- - TypeScript support with strictest config
14
- - UI, component, and end-to-end testing
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
19
- - Sample tests (headless & headed modes)
20
- - Scalable folder structure with best practices
21
- - WebServer configuration for running tests against local servers
22
- - Complete package.json with all required scripts
23
- - Modern ES modules with latest imports
24
- - Full TypeScript integration with types
25
- - Minimal setup - Developers only write test steps & scenarios
26
-
27
- ## Prerequisites
28
- - Node.js 18+ installed
29
- - npm or yarn
30
-
31
- ## Setup Instructions
32
-
33
- ### 1. Install Dependencies (Chromium Only + TypeScript)
34
- ```bash
35
- npm install
36
- npm install --save-dev typescript @types/node @playwright/test
37
- npx playwright install chromium
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
- npm install --save-dev @cucumber/cucumber csv-parser js-yaml @types/js-yaml
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
- ### 3. Required package.json Scripts
47
- Ensure your `package.json` includes these scripts:
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
- ### 4. TypeScript Configuration (`tsconfig.json`)
63
- ```json
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
- ### 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
- ```
87
+ ## Core Components
126
88
 
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/',
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
- ### 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
- },
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 fs from 'fs';
178
- import path from 'path';
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 DATA_DIR = path.join(process.cwd(), 'tests', 'data');
125
+ export const wrapperClient = {
126
+ createSchedulingGroup: async (payload: any, user: any) => {
127
+ const correlationId = utils.getCorrelationId();
183
128
 
184
- interface DataRecord {
185
- [key: string]: string | number | boolean;
186
- }
129
+ const headers = {
130
+ "x-correlation-id": correlationId,
131
+ "x-test-user-id": user.id
132
+ };
187
133
 
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
- }
134
+ utils.log("Calling wrapper API", { payload, user });
195
135
 
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
- }
136
+ const response = await axios.post(
137
+ `${process.env.WRAPPER_URL}/scheduling-groups`,
138
+ payload,
139
+ { headers }
140
+ );
202
141
 
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
- }
142
+ utils.log("Wrapper API response", response.data);
215
143
 
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;
144
+ return { ...response.data, correlationId };
226
145
  }
227
- }
146
+ };
228
147
  ```
229
148
 
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
- ```
149
+ ### Utilities `core/utils/correlation.ts`
343
150
 
344
- ### 10. Test Data Manager (`tests/utils/testData.ts`) - TypeScript
345
151
  ```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
- }
152
+ import { v4 as uuidv4 } from "uuid";
390
153
 
391
- // Load form data from CSV
392
- static async loadFormData(): Promise<FormData[]> {
393
- return await DataReader.readCSV('formData.csv') as unknown as FormData[];
394
- }
154
+ export const utils = {
155
+ getCorrelationId: (): string => uuidv4(),
395
156
 
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
- }
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
- // 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);
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
- **`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'
169
+ ### Fixtures – `core/fixtures/context.fixture.ts`
471
170
 
472
- ```
473
-
474
- ### 10. Enhanced Page Object with Form Helper (`tests/pages/FormPage.js`)
475
- ```javascript
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
- }
171
+ ```typescript
172
+ export const contextProvider = {
173
+ getCurrentUser: async () => {
174
+ return { id: "default-system-admin", role: "System Admin" };
175
+ },
486
176
 
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
- }
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
- ### 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
- }
188
+ ## Test Data & Database
552
189
 
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
- ```
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
- 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 };
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
- export const test = base.extend<AuthFixtures>({
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
- inventoryPage: async ({ page }, use) => {
624
- const inventoryPage = new InventoryPage(page);
625
- await use(inventoryPage);
626
- },
206
+ ```typescript
207
+ import { dbClient } from "../../core/test-harness/dbClient";
627
208
 
628
- formPage: async ({ page }, use) => {
629
- const formPage = new FormPage(page);
630
- await use(formPage);
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
- authenticatedUser: async ({ page, loginPage }, use) => {
634
- await loginPage.goto();
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 waitForElement(selector: string, timeout: number = 5000): Promise<void> {
675
- await this.page.waitForSelector(selector, { timeout });
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
- ### 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
- ```
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 { 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"]';
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
- async addProductToCart(): Promise<void> {
722
- await this.click(this.addToCartBtn);
723
- }
248
+ const groupId = result[0].Id;
724
249
 
725
- async getCartCount(): Promise<string | null> {
726
- return await this.getText(this.cartBadge);
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
- async isProductAdded(): Promise<string | null> {
730
- await this.waitForElement(this.cartBadge);
731
- return await this.getText(this.cartBadge);
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
- 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/');
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
- ### 18. Run Tests
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
- # Run with custom worker count
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, 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
- });
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('I navigate to the SauceDemo application', async function() {
883
- await loginPage.goto();
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
- When('I login with username {string} and password {string}', async function(username: string, password: string) {
887
- await loginPage.login(username, password);
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
- Then('I should see the inventory page', async function() {
891
- await page.waitForURL('**/inventory.html');
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('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
- });
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
- 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
- });
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
- 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
- });
315
+ const teams = await schedulingGroupRead.getTeamsByGroupId(this.context.groupId);
316
+ expect(teams.map(t => t.TeamId)).toEqual(this.context.payload.teams);
916
317
 
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/');
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
- 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
- ```
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: 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
327
+ Feature: Create Scheduling Group
1108
328
 
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
- });
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
- ### 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
- ```
336
+ ## Database Scripts
1241
337
 
1242
- ## Developer Quick Start
338
+ ### Seed Script – `scripts/seed-db.ts`
1243
339
 
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
- ```
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
- **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']);
352
+ seed();
1259
353
  ```
1260
354
 
1261
- **Step 4:** Create page objects that combine both:
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 { TestDataManager } from '@utils/testData';
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
- // Get form data by scenario
1339
- const formData = await TestDataManager.getFormDataByScenario('registration_valid');
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
- // Load all config with type safety
1342
- const config = TestDataManager.loadConfig();
367
+ reset();
1343
368
  ```
1344
369
 
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');
370
+ ## Package Configuration
1357
371
 
1358
- // Radio button
1359
- await formHelper.selectRadioButton('Male');
372
+ ### package.json Scripts
1360
373
 
1361
- // Single checkbox
1362
- await formHelper.checkCheckbox('I accept terms');
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
- // Multiple checkboxes
1365
- await formHelper.checkMultiple(['Option1', 'Option2', 'Option3']);
386
+ ## Running Tests
1366
387
 
1367
- // Get and verify values (fully typed)
1368
- const firstName = await formHelper.getInputValue('#firstName');
1369
- const isChecked = await formHelper.isCheckboxChecked('I accept terms');
1370
- ```
388
+ 1. **Reset the database:**
389
+ ```bash
390
+ npm run reset-db
391
+ ```
1371
392
 
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
- ```
393
+ 2. **Seed the database:**
394
+ ```bash
395
+ npm run seed-db
396
+ ```
1392
397
 
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
- ```
398
+ 3. **Run API/DB tests:**
399
+ ```bash
400
+ npm run apitest
401
+ ```
1404
402
 
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.
403
+ 4. **View reports and logs:**
404
+ Check the `reports/` directory for HTML reports and detailed logs.