agents-cli-automation 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2989 @@
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.
6
+
7
+ ---
8
+
9
+ ## BBC Business Context & Requirements
10
+
11
+ ### Scheduling Group Feature Overview
12
+
13
+ **Business Context:**
14
+ Scheduling Group is a new grouping function that allows Scheduling Teams within an Area to be grouped together. One Scheduling Team can be in more than one Scheduling Group. One Scheduling Group can have one or more Scheduling Teams in it. A Scheduling Team doesn't have to be in a Scheduling Group. Initially, this allows improvements such as presenting the Allocations Menu in a more coherent way and giving permissions to Scheduling Teams more efficiently.
15
+
16
+ **Key Business Rules:**
17
+ - Area Admin can manage Scheduling Groups only for their assigned Areas
18
+ - System Admin can manage Scheduling Groups across all Areas
19
+ - Scheduling Group Name is editable
20
+ - Area is auto-filled and non-editable after initial selection
21
+ - Allocations Menu: Yes/No toggle (default behavior TBD by business)
22
+ - Associated Scheduling Teams fed from system (multiple teams per group)
23
+ - History tracking required for all changes (audit trail)
24
+ - Notes field for free-text annotations
25
+ - Delete confirmation required with group name
26
+
27
+ **Column Headings:**
28
+ - **Actions**: Edit, Delete, History
29
+ - **Area**: Non-editable after first choice
30
+ - **Scheduling Group Name**: Editable
31
+ - **Associated Scheduling Teams**: From system (multi-select)
32
+ - **Allocations Menu**: Yes/No with hover tooltip
33
+ - **Notes**: Editable, free text
34
+ - **Last Amended Date**: From system (auto)
35
+ - **Last Amended By**: From system (audit)
36
+
37
+ ### BBC User Stories
38
+
39
+ | Reference | Role(s) | Function | Benefit |
40
+ |-----------|---------|----------|---------|
41
+ | NP035.01 | Area Admin, System Admin | View list of Scheduling Groups in Area | See which Scheduling Groups already exist |
42
+ | NP035.02 | Area Admin, System Admin | Add NEW Scheduling Group in Area | Provide consistent and up-to-date group list |
43
+ | NP035.03 | Area Admin, System Admin | Edit existing Scheduling Group in Area | Maintain and update group information |
44
+ | NP035.04 | Area Admin, System Admin | Delete existing Scheduling Group in Area | Remove outdated or unnecessary groups |
45
+ | NP035.05 | Area Admin, System Admin | View history of Scheduling Group changes | Track all amendments and audit trail |
46
+
47
+ ### Required Business Outcomes
48
+
49
+ ✅ Area Admin can View, Add, Edit, Delete Scheduling Groups for their Area
50
+ ✅ System Admin can View, Add, Edit, Delete Scheduling Groups across all Areas
51
+ ✅ History of changes available covering all column headings
52
+ ✅ Columns are sortable and filterable
53
+ ✅ Allocations Menu impacts visibility in menus (impacts orphaned teams consideration)
54
+ ✅ Permission validation enforced (Area Admin limited to their areas)
55
+ ✅ Audit trail captures who changed what and when
56
+
57
+ ---
58
+
59
+ ## Folder Structure
60
+
61
+ ```
62
+ automation-framework/
63
+ ├─ package.json
64
+ ├─ tsconfig.json
65
+ ├─ playwright.config.ts
66
+ ├─ .env
67
+
68
+ ├─ configs/
69
+ │ ├─ projects/
70
+ │ │ ├─ ui.project.ts
71
+ │ │ ├─ api.project.ts
72
+ │ │ ├─ db.project.ts
73
+ │ │ └─ integrated.project.ts
74
+ │ ├─ environments/
75
+ │ └─ global-setup.ts
76
+
77
+ ├─ scripts/
78
+ │ ├─ seed-db.ts
79
+ │ ├─ reset-db.ts
80
+ │ └─ run-wrapper.ts
81
+
82
+ ├─ src/
83
+ │ ├─ core/
84
+ │ │ ├─ test-harness/
85
+ │ │ │ ├─ dbClient.ts
86
+ │ │ │ └─ wrapperClient.ts
87
+ │ │ ├─ fixtures/
88
+ │ │ │ └─ context.fixture.ts
89
+ │ │ └─ utils/
90
+ │ │ ├─ logger.ts
91
+ │ │ └─ correlation.ts
92
+ │ │
93
+ │ ├─ modules/
94
+ │ │ ├─ scheduling-group/
95
+ │ │ │ ├─ features/
96
+ │ │ │ │ └─ create-scheduling-group.feature
97
+ │ │ │ ├─ steps/
98
+ │ │ │ │ └─ createSchedulingGroup.steps.ts
99
+ │ │ │ ├─ read-models/
100
+ │ │ │ │ └─ schedule.read.ts
101
+ │ │ │ ├─ queries/
102
+ │ │ │ │ └─ schedule.mutations.ts
103
+ │ │ │ ├─ contracts/
104
+ │ │ │ │ └─ schedule.wrapper.contract.ts
105
+ │ │ │ └─ test-data/
106
+ │ │ │ └─ schedule.seed.ts
107
+ │ │ └─ <future-module>/
108
+ │ │
109
+ │ └─ shared/
110
+ │ ├─ constants/
111
+ │ ├─ types/
112
+ │ └─ data-factory/
113
+
114
+ ├─ reports/
115
+ └─ README.md
116
+ ```
117
+
118
+ ## Environment Configuration
119
+
120
+ ### .env
121
+
122
+ ```bash
123
+ # Database
124
+ DB_HOST=localhost
125
+ DB_PORT=1433
126
+ DB_USER=sa
127
+ DB_PASSWORD=YourStrong@Passw0rd
128
+ DB_NAME=AutomationTestDB
129
+
130
+ # Wrapper / API
131
+ WRAPPER_URL=http://localhost:4000/test-wrapper
132
+ ```
133
+
134
+ ## Core Components
135
+
136
+ ### Playwright Configuration – `playwright.config.ts`
137
+
138
+ ```typescript
139
+ import { defineConfig, devices } from '@playwright/test';
140
+
141
+ export default defineConfig({
142
+ testDir: './src/modules',
143
+ testMatch: '**/*.spec.ts',
144
+ fullyParallel: true,
145
+ forbidOnly: !!process.env.CI,
146
+ retries: process.env.CI ? 2 : 0,
147
+ workers: process.env.CI ? 1 : undefined,
148
+ reporter: [
149
+ ['html', { open: 'never', outputFolder: './reports/html' }],
150
+ ['json', { outputFile: './reports/results.json' }],
151
+ ['junit', { outputFile: './reports/junit.xml' }],
152
+ ['list']
153
+ ],
154
+ use: {
155
+ baseURL: process.env.WRAPPER_URL || 'http://localhost:4000',
156
+ trace: 'on-first-retry',
157
+ },
158
+ projects: [
159
+ {
160
+ name: 'ui',
161
+ use: { ...devices['chromium'] },
162
+ testMatch: '**/ui/**/*.spec.ts',
163
+ },
164
+ {
165
+ name: 'api',
166
+ use: { baseURL: process.env.WRAPPER_URL || 'http://localhost:4000' },
167
+ testMatch: '**/api/**/*.spec.ts',
168
+ },
169
+ {
170
+ name: 'db',
171
+ testMatch: '**/db/**/*.spec.ts',
172
+ },
173
+ {
174
+ name: 'integrated',
175
+ use: { ...devices['chromium'] },
176
+ testMatch: '**/integrated/**/*.spec.ts',
177
+ },
178
+ ],
179
+ });
180
+ ```
181
+
182
+ **Integration with bddgen:**
183
+
184
+ - **testDir**: Points to modules directory where bddgen generates step files
185
+ - **testMatch**: Only runs `.spec.ts` files (not `.feature` or `.steps.ts` files)
186
+ - **reporters**: Generates reports compatible with bddgen cucumber reports in `reports/` directory
187
+ - **Complementary Role**: Playwright config handles browser automation and retries; Cucumber/bddgen handle BDD test orchestration
188
+ - **Test Flow**: Feature files → bddgen generates steps → Cucumber runs steps → Playwright handles browser/API actions
189
+
190
+
191
+ ### TypeScript Configuration – `tsconfig.json`
192
+
193
+ ```json
194
+ {
195
+ "compilerOptions": {
196
+ "target": "ES2020",
197
+ "module": "commonjs",
198
+ "lib": ["ES2020"],
199
+ "outDir": "./dist",
200
+ "rootDir": "./",
201
+ "strict": true,
202
+ "esModuleInterop": true,
203
+ "skipLibCheck": true,
204
+ "forceConsistentCasingInFileNames": true,
205
+ "resolveJsonModule": true,
206
+ "declaration": true,
207
+ "declarationMap": true,
208
+ "sourceMap": true,
209
+ "moduleResolution": "node",
210
+ "allowSyntheticDefaultImports": true,
211
+ "baseUrl": ".",
212
+ "paths": {
213
+ "@core/*": ["src/core/*"],
214
+ "@modules/*": ["src/modules/*"],
215
+ "@shared/*": ["src/shared/*"],
216
+ "@fixtures/*": ["src/core/fixtures/*"],
217
+ "@utils/*": ["src/core/utils/*"],
218
+ "@test-harness/*": ["src/core/test-harness/*"]
219
+ }
220
+ },
221
+ "include": ["src/**/*", "scripts/**/*", "configs/**/*"],
222
+ "exclude": ["node_modules", "dist", "reports", "src/modules/**/*.spec.ts"]
223
+ }
224
+ ```
225
+
226
+ ## Core Components
227
+
228
+ ### DB Client – `core/test-harness/dbClient.ts`
229
+
230
+ ```typescript
231
+ import { ConnectionPool, config as sqlConfig, Request } from "mssql";
232
+ import dotenv from "dotenv";
233
+ import { logger } from "../utils/logger";
234
+
235
+ dotenv.config();
236
+
237
+ let pool: ConnectionPool | null = null;
238
+
239
+ const getPool = async (): Promise<ConnectionPool> => {
240
+ if (!pool) {
241
+ const config: sqlConfig = {
242
+ user: process.env.DB_USER,
243
+ password: process.env.DB_PASSWORD,
244
+ server: process.env.DB_HOST || "localhost",
245
+ port: parseInt(process.env.DB_PORT || "1433"),
246
+ database: process.env.DB_NAME,
247
+ options: {
248
+ encrypt: true,
249
+ trustServerCertificate: true,
250
+ enableKeepAlive: true,
251
+ },
252
+ pool: {
253
+ max: 10,
254
+ min: 2,
255
+ idleTimeoutMillis: 30000,
256
+ },
257
+ };
258
+
259
+ pool = new ConnectionPool(config);
260
+ await pool.connect();
261
+ logger.info("Database connection pool established");
262
+ }
263
+ return pool;
264
+ };
265
+
266
+ export const dbClient = {
267
+ query: async (queryStr: string, params?: Record<string, any>): Promise<any[]> => {
268
+ try {
269
+ const connection = await getPool();
270
+ const request = connection.request();
271
+
272
+ if (params) {
273
+ for (const key in params) {
274
+ const value = params[key];
275
+ request.input(key, value);
276
+ }
277
+ }
278
+
279
+ logger.debug("Executing query", { queryStr, params });
280
+ const result = await request.query(queryStr);
281
+ return result.recordset || [];
282
+ } catch (error) {
283
+ logger.error("Database query failed", error);
284
+ throw error;
285
+ }
286
+ },
287
+
288
+ execute: async (procedureName: string, params?: Record<string, any>): Promise<any> => {
289
+ try {
290
+ const connection = await getPool();
291
+ const request = connection.request();
292
+
293
+ if (params) {
294
+ for (const key in params) {
295
+ const value = params[key];
296
+ request.input(key, value);
297
+ }
298
+ }
299
+
300
+ logger.debug("Executing procedure", { procedureName, params });
301
+ const result = await request.execute(procedureName);
302
+ return result;
303
+ } catch (error) {
304
+ logger.error("Procedure execution failed", error);
305
+ throw error;
306
+ }
307
+ },
308
+
309
+ close: async () => {
310
+ if (pool) {
311
+ await pool.close();
312
+ pool = null;
313
+ logger.info("Database connection closed");
314
+ }
315
+ },
316
+ };
317
+ ```
318
+
319
+ ### Wrapper Client / API Layer – `core/test-harness/wrapperClient.ts`
320
+
321
+ ```typescript
322
+ import axios, { AxiosInstance, AxiosResponse } from "axios";
323
+ import { utils } from "../utils/correlation";
324
+ import { logger } from "../utils/logger";
325
+
326
+ class WrapperClient {
327
+ private client: AxiosInstance;
328
+ private baseURL: string;
329
+
330
+ constructor(baseURL: string = process.env.WRAPPER_URL || "http://localhost:4000/test-wrapper") {
331
+ this.baseURL = baseURL;
332
+ this.client = axios.create({
333
+ baseURL: this.baseURL,
334
+ timeout: 30000,
335
+ headers: {
336
+ "Content-Type": "application/json",
337
+ },
338
+ });
339
+ }
340
+
341
+ private getHeaders(user?: any): Record<string, string> {
342
+ const headers: Record<string, string> = {
343
+ "x-correlation-id": utils.getCorrelationId(),
344
+ };
345
+
346
+ if (user?.id) {
347
+ headers["x-test-user-id"] = user.id;
348
+ }
349
+
350
+ return headers;
351
+ }
352
+
353
+ async createSchedulingGroup(payload: any, user?: any): Promise<any> {
354
+ try {
355
+ logger.info("Creating Scheduling Group via Wrapper API", { payload, user });
356
+
357
+ const response: AxiosResponse = await this.client.post(
358
+ "/scheduling-groups",
359
+ payload,
360
+ { headers: this.getHeaders(user) }
361
+ );
362
+
363
+ logger.info("Scheduling Group created successfully", response.data);
364
+ return response.data;
365
+ } catch (error: any) {
366
+ logger.error("Failed to create Scheduling Group", error.response?.data || error.message);
367
+ throw error;
368
+ }
369
+ }
370
+
371
+ async getSchedulingGroup(groupId: number, user?: any): Promise<any> {
372
+ try {
373
+ logger.info("Getting Scheduling Group", { groupId });
374
+
375
+ const response: AxiosResponse = await this.client.get(
376
+ `/scheduling-groups/${groupId}`,
377
+ { headers: this.getHeaders(user) }
378
+ );
379
+
380
+ return response.data;
381
+ } catch (error: any) {
382
+ logger.error("Failed to get Scheduling Group", error.response?.data || error.message);
383
+ throw error;
384
+ }
385
+ }
386
+
387
+ async updateSchedulingGroup(groupId: number, payload: any, user?: any): Promise<any> {
388
+ try {
389
+ logger.info("Updating Scheduling Group", { groupId, payload });
390
+
391
+ const response: AxiosResponse = await this.client.put(
392
+ `/scheduling-groups/${groupId}`,
393
+ payload,
394
+ { headers: this.getHeaders(user) }
395
+ );
396
+
397
+ return response.data;
398
+ } catch (error: any) {
399
+ logger.error("Failed to update Scheduling Group", error.response?.data || error.message);
400
+ throw error;
401
+ }
402
+ }
403
+
404
+ async deleteSchedulingGroup(groupId: number, user?: any): Promise<any> {
405
+ try {
406
+ logger.info("Deleting Scheduling Group", { groupId });
407
+
408
+ const response: AxiosResponse = await this.client.delete(
409
+ `/scheduling-groups/${groupId}`,
410
+ { headers: this.getHeaders(user) }
411
+ );
412
+
413
+ return response.data;
414
+ } catch (error: any) {
415
+ logger.error("Failed to delete Scheduling Group", error.response?.data || error.message);
416
+ throw error;
417
+ }
418
+ }
419
+ }
420
+
421
+ export const wrapperClient = new WrapperClient();
422
+ ```
423
+
424
+ ### Utilities – `core/utils/correlation.ts`
425
+
426
+ ```typescript
427
+ import { v4 as uuidv4 } from "uuid";
428
+ import { logger } from "./logger";
429
+
430
+ export const utils = {
431
+ getCorrelationId: (): string => uuidv4(),
432
+
433
+ normalizeTimestamps: (record: any) => {
434
+ if (record?.createdAt) record.createdAt = "<timestamp>";
435
+ if (record?.updatedAt) record.updatedAt = "<timestamp>";
436
+ if (record?.LastAmendedDate) record.LastAmendedDate = "<timestamp>";
437
+ return record;
438
+ },
439
+
440
+ log: (message: string, data?: any) => {
441
+ logger.info(message, data);
442
+ },
443
+
444
+ delay: async (milliseconds: number) => {
445
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
446
+ },
447
+
448
+ retry: async (fn: () => Promise<any>, retries: number = 3, delay: number = 1000) => {
449
+ let lastError;
450
+ for (let i = 0; i < retries; i++) {
451
+ try {
452
+ return await fn();
453
+ } catch (error) {
454
+ lastError = error;
455
+ logger.warn(`Retry attempt ${i + 1} failed, waiting ${delay}ms before next attempt`);
456
+ await utils.delay(delay);
457
+ }
458
+ }
459
+ throw lastError;
460
+ }
461
+ };
462
+ ```
463
+
464
+ ### Logger – `core/utils/logger.ts`
465
+
466
+ ```typescript
467
+ export const logger = {
468
+ info: (message: string, data?: any) => {
469
+ console.log(`[${new Date().toISOString()}] [INFO] ${message}`, data ? JSON.stringify(data) : "");
470
+ },
471
+
472
+ error: (message: string, error?: any) => {
473
+ console.error(`[${new Date().toISOString()}] [ERROR] ${message}`, error ? JSON.stringify(error) : "");
474
+ },
475
+
476
+ debug: (message: string, data?: any) => {
477
+ if (process.env.DEBUG) {
478
+ console.debug(`[${new Date().toISOString()}] [DEBUG] ${message}`, data ? JSON.stringify(data) : "");
479
+ }
480
+ },
481
+
482
+ warn: (message: string, data?: any) => {
483
+ console.warn(`[${new Date().toISOString()}] [WARN] ${message}`, data ? JSON.stringify(data) : "");
484
+ }
485
+ };
486
+ ```
487
+
488
+ ### Fixtures – `core/fixtures/context.fixture.ts`
489
+
490
+ ```typescript
491
+ export interface TestContext {
492
+ user: any;
493
+ payload: any;
494
+ response: any;
495
+ correlationId: string;
496
+ groupId?: number;
497
+ [key: string]: any;
498
+ }
499
+
500
+ export interface User {
501
+ id: string;
502
+ role: string;
503
+ name?: string;
504
+ }
505
+
506
+ export const contextProvider = {
507
+ getCurrentUser: async (): Promise<User> => {
508
+ return {
509
+ id: "default-system-admin",
510
+ role: "System Admin",
511
+ name: "Test Admin",
512
+ };
513
+ },
514
+
515
+ getAreaAdminUser: async (): Promise<User> => {
516
+ return {
517
+ id: "area-admin-user-1",
518
+ role: "Area Admin",
519
+ name: "Area Admin User",
520
+ };
521
+ },
522
+
523
+ getRegularUser: async (): Promise<User> => {
524
+ return {
525
+ id: "regular-user-1",
526
+ role: "Regular User",
527
+ name: "Regular Test User",
528
+ };
529
+ },
530
+
531
+ createTestContext: (): TestContext => {
532
+ return {
533
+ user: null,
534
+ payload: null,
535
+ response: null,
536
+ correlationId: "",
537
+ };
538
+ },
539
+
540
+ setupContextWithUser: async (context: TestContext, userType: "admin" | "area-admin" | "regular" = "admin"): Promise<TestContext> => {
541
+ switch (userType) {
542
+ case "area-admin":
543
+ context.user = await contextProvider.getAreaAdminUser();
544
+ break;
545
+ case "regular":
546
+ context.user = await contextProvider.getRegularUser();
547
+ break;
548
+ default:
549
+ context.user = await contextProvider.getCurrentUser();
550
+ }
551
+ return context;
552
+ },
553
+ };
554
+ ```
555
+
556
+ ### Fixture Usage in BDD – How It Works
557
+
558
+ Fixtures are injected into BDD steps through the **World context**. Here's how the integration works:
559
+
560
+ ```typescript
561
+ // In Before hook - fixtures are initialized
562
+ Before(async function (this: SchedulingGroupWorld) {
563
+ // contextProvider creates fresh context for each scenario
564
+ this.context = contextProvider.createTestContext();
565
+ this.user = await contextProvider.getCurrentUser();
566
+ });
567
+
568
+ // In Given steps - fixtures are accessed and configured
569
+ Given("an Area Admin user is available", async function (this: SchedulingGroupWorld) {
570
+ // Use contextProvider to setup specific user type
571
+ this.user = await contextProvider.getAreaAdminUser();
572
+ this.context.user = this.user;
573
+ });
574
+
575
+ // In When/Then steps - use the injected context
576
+ When("the user creates...", async function (this: SchedulingGroupWorld) {
577
+ // Access context set up by fixtures
578
+ const groupId = await schedulingGroupMutations.createSchedulingGroupStub(
579
+ this.context.payload,
580
+ this.user.id // user from fixture
581
+ );
582
+ });
583
+
584
+ // In After hook - cleanup resources
585
+ After(async function (this: SchedulingGroupWorld) {
586
+ // Context is automatically cleaned up
587
+ if (this.groupId) {
588
+ await schedulingGroupMutations.deleteSchedulingGroupStub(this.groupId);
589
+ }
590
+ });
591
+ ```
592
+
593
+ **Benefits of This Approach:**
594
+ - ✅ Single source of truth for test data (builders)
595
+ - ✅ Reusable user contexts across scenarios
596
+ - ✅ Automatic lifecycle management (Before/After)
597
+ - ✅ Type-safe context injection (World interface)
598
+ - ✅ Clear separation between setup (fixtures) and execution (steps)
599
+ ```
600
+
601
+ ## Test Data & Database
602
+
603
+ ### Test Data Builder – `modules/scheduling-group/test-data/schedule.seed.ts`
604
+
605
+ ```typescript
606
+ export interface SchedulingGroupPayload {
607
+ name: string;
608
+ areaId: number;
609
+ allocationsMenu: boolean;
610
+ notes: string;
611
+ teams: number[];
612
+ }
613
+
614
+ export const schedulingGroupBuilder = {
615
+ createValid: (): SchedulingGroupPayload => ({
616
+ name: `MorningNews_${Date.now()}`,
617
+ areaId: 101,
618
+ allocationsMenu: true,
619
+ notes: "Used for weekday planning",
620
+ teams: [201, 202],
621
+ }),
622
+
623
+ createWithCustomName: (name: string): SchedulingGroupPayload => ({
624
+ ...schedulingGroupBuilder.createValid(),
625
+ name,
626
+ }),
627
+
628
+ createWithCustomArea: (areaId: number): SchedulingGroupPayload => ({
629
+ ...schedulingGroupBuilder.createValid(),
630
+ areaId,
631
+ }),
632
+
633
+ createWithCustomTeams: (teams: number[]): SchedulingGroupPayload => ({
634
+ ...schedulingGroupBuilder.createValid(),
635
+ teams,
636
+ }),
637
+
638
+ createMinimal: (): SchedulingGroupPayload => ({
639
+ name: `TestGroup_${Date.now()}`,
640
+ areaId: 101,
641
+ allocationsMenu: false,
642
+ notes: "",
643
+ teams: [],
644
+ }),
645
+
646
+ createWithAllOptions: (overrides?: Partial<SchedulingGroupPayload>): SchedulingGroupPayload => ({
647
+ ...schedulingGroupBuilder.createValid(),
648
+ ...overrides,
649
+ }),
650
+ };
651
+ ```
652
+
653
+ ### Read-Models – `modules/scheduling-group/read-models/schedule.read.ts`
654
+
655
+ ```typescript
656
+ import { dbClient } from "@test-harness/dbClient";
657
+ import { logger } from "@utils/logger";
658
+
659
+ export const schedulingGroupRead = {
660
+ getByName: async (name: string): Promise<any> => {
661
+ try {
662
+ logger.info("Querying SchedulingGroup by name", { name });
663
+ const result = await dbClient.query(
664
+ `SELECT * FROM SchedulingGroup WHERE Name = @name`,
665
+ { name }
666
+ );
667
+ return result[0] || null;
668
+ } catch (error) {
669
+ logger.error("Failed to get SchedulingGroup by name", error);
670
+ throw error;
671
+ }
672
+ },
673
+
674
+ getById: async (id: number): Promise<any> => {
675
+ try {
676
+ logger.info("Querying SchedulingGroup by id", { id });
677
+ const result = await dbClient.query(
678
+ `SELECT * FROM SchedulingGroup WHERE Id = @id`,
679
+ { id }
680
+ );
681
+ return result[0] || null;
682
+ } catch (error) {
683
+ logger.error("Failed to get SchedulingGroup by id", error);
684
+ throw error;
685
+ }
686
+ },
687
+
688
+ getTeamsByGroupId: async (groupId: number): Promise<any[]> => {
689
+ try {
690
+ logger.info("Querying teams for SchedulingGroup", { groupId });
691
+ return await dbClient.query(
692
+ `SELECT * FROM SchedulingGroupTeam WHERE SchedulingGroupId = @groupId`,
693
+ { groupId }
694
+ );
695
+ } catch (error) {
696
+ logger.error("Failed to get teams for SchedulingGroup", error);
697
+ throw error;
698
+ }
699
+ },
700
+
701
+ getHistoryByGroupId: async (groupId: number): Promise<any[]> => {
702
+ try {
703
+ logger.info("Querying history for SchedulingGroup", { groupId });
704
+ return await dbClient.query(
705
+ `SELECT * FROM SchedulingGroupHistory WHERE SchedulingGroupId = @groupId ORDER BY LastAmendedDate DESC`,
706
+ { groupId }
707
+ );
708
+ } catch (error) {
709
+ logger.error("Failed to get history for SchedulingGroup", error);
710
+ throw error;
711
+ }
712
+ },
713
+
714
+ getAllGroups: async (): Promise<any[]> => {
715
+ try {
716
+ logger.info("Querying all SchedulingGroups");
717
+ return await dbClient.query(`SELECT * FROM SchedulingGroup`);
718
+ } catch (error) {
719
+ logger.error("Failed to get all SchedulingGroups", error);
720
+ throw error;
721
+ }
722
+ },
723
+
724
+ getByAreaId: async (areaId: number): Promise<any[]> => {
725
+ try {
726
+ logger.info("Querying SchedulingGroups by AreaId", { areaId });
727
+ return await dbClient.query(
728
+ `SELECT * FROM SchedulingGroup WHERE AreaId = @areaId`,
729
+ { areaId }
730
+ );
731
+ } catch (error) {
732
+ logger.error("Failed to get SchedulingGroups by AreaId", error);
733
+ throw error;
734
+ }
735
+ },
736
+ };
737
+ ```
738
+
739
+ ### DB Stub / Invariants – `modules/scheduling-group/queries/schedule.mutations.ts`
740
+
741
+ ```typescript
742
+ import { dbClient } from "@test-harness/dbClient";
743
+ import { logger } from "@utils/logger";
744
+ import { SchedulingGroupPayload } from "../test-data/schedule.seed";
745
+
746
+ export const schedulingGroupMutations = {
747
+ // CREATE: Insert Scheduling Group with full audit trail
748
+ createSchedulingGroupStub: async (
749
+ payload: SchedulingGroupPayload,
750
+ userId: string,
751
+ correlationId?: string
752
+ ): Promise<number> => {
753
+ try {
754
+ logger.info("Creating SchedulingGroup stub in database", {
755
+ payload,
756
+ userId,
757
+ correlationId,
758
+ });
759
+
760
+ // Validation: Check for duplicate name in same area
761
+ const duplicate = await dbClient.query(
762
+ `SELECT COUNT(*) as cnt FROM SchedulingGroup WHERE AreaId = @areaId AND Name = @name`,
763
+ { areaId: payload.areaId, name: payload.name }
764
+ );
765
+
766
+ if (duplicate[0]?.cnt > 0) {
767
+ logger.error("Duplicate SchedulingGroup name in area", { areaId: payload.areaId, name: payload.name });
768
+ throw new Error(
769
+ `Scheduling Group with name '${payload.name}' already exists in this Area`
770
+ );
771
+ }
772
+
773
+ // Validation: Check area exists
774
+ const area = await dbClient.query(
775
+ `SELECT Id FROM Area WHERE Id = @areaId`,
776
+ { areaId: payload.areaId }
777
+ );
778
+ if (!area.length) {
779
+ throw new Error(`Area with ID ${payload.areaId} does not exist`);
780
+ }
781
+
782
+ // Validation: Check all team IDs exist and belong to area
783
+ if (payload.teams.length > 0) {
784
+ const teamCount = await dbClient.query(
785
+ `SELECT COUNT(*) as cnt FROM SchedulingTeam WHERE AreaId = @areaId AND Id IN (${payload.teams.join(
786
+ ","
787
+ )})`,
788
+ { areaId: payload.areaId }
789
+ );
790
+ if (teamCount[0]?.cnt !== payload.teams.length) {
791
+ throw new Error(
792
+ `One or more team IDs do not exist in Area ${payload.areaId}`
793
+ );
794
+ }
795
+ }
796
+
797
+ // Create SchedulingGroup
798
+ const result = await dbClient.query(
799
+ `INSERT INTO SchedulingGroup (AreaId, Name, AllocationsMenu, Notes, LastAmendedBy, LastAmendedDate)
800
+ VALUES (@areaId, @name, @allocationsMenu, @notes, @userId, GETDATE());
801
+ SELECT SCOPE_IDENTITY() AS Id;`,
802
+ {
803
+ areaId: payload.areaId,
804
+ name: payload.name,
805
+ allocationsMenu: payload.allocationsMenu,
806
+ notes: payload.notes,
807
+ userId,
808
+ }
809
+ );
810
+
811
+ const groupId = result[0]?.Id;
812
+ if (!groupId) {
813
+ throw new Error("Failed to retrieve inserted Scheduling Group ID");
814
+ }
815
+ logger.info("SchedulingGroup created with Id", { groupId, correlationId });
816
+
817
+ // Link teams to group (many-to-many)
818
+ for (const teamId of payload.teams) {
819
+ await dbClient.query(
820
+ `INSERT INTO SchedulingGroupTeam (SchedulingGroupId, TeamId) VALUES (@groupId, @teamId)`,
821
+ { groupId, teamId }
822
+ );
823
+ }
824
+ logger.info("Teams linked to SchedulingGroup", {
825
+ groupId,
826
+ teamCount: payload.teams.length,
827
+ correlationId,
828
+ });
829
+
830
+ // Create audit history record
831
+ await dbClient.query(
832
+ `INSERT INTO SchedulingGroupHistory (SchedulingGroupId, AreaId, Name, AllocationsMenu, Notes, LastAmendedBy, LastAmendedDate, Operation)
833
+ VALUES (@groupId, @areaId, @name, @allocationsMenu, @notes, @userId, GETDATE(), 'CREATE')`,
834
+ {
835
+ groupId,
836
+ areaId: payload.areaId,
837
+ name: payload.name,
838
+ allocationsMenu: payload.allocationsMenu,
839
+ notes: payload.notes,
840
+ userId,
841
+ }
842
+ );
843
+ logger.info("Audit history record created", { groupId, correlationId });
844
+
845
+ return groupId;
846
+ } catch (error) {
847
+ logger.error("Failed to create SchedulingGroup stub", { error, correlationId });
848
+ throw error;
849
+ }
850
+ },
851
+
852
+ // UPDATE: Modify existing Scheduling Group with history tracking
853
+ updateSchedulingGroupStub: async (
854
+ groupId: number,
855
+ payload: Partial<SchedulingGroupPayload>,
856
+ userId: string,
857
+ correlationId?: string
858
+ ): Promise<void> => {
859
+ try {
860
+ logger.info("Updating SchedulingGroup stub in database", {
861
+ groupId,
862
+ payload,
863
+ userId,
864
+ correlationId,
865
+ });
866
+
867
+ // Fetch current state for history
868
+ const current = await dbClient.query(
869
+ `SELECT AreaId, Name, AllocationsMenu, Notes FROM SchedulingGroup WHERE Id = @groupId`,
870
+ { groupId }
871
+ );
872
+ if (!current.length) {
873
+ throw new Error(`SchedulingGroup with ID ${groupId} not found`);
874
+ }
875
+
876
+ const updates: string[] = [];
877
+ const params: Record<string, any> = { groupId, userId };
878
+
879
+ if (payload.name !== undefined) {
880
+ // Check for duplicate name if changing
881
+ if (payload.name !== current[0].Name) {
882
+ const duplicate = await dbClient.query(
883
+ `SELECT COUNT(*) as cnt FROM SchedulingGroup WHERE AreaId = @areaId AND Name = @name AND Id != @groupId`,
884
+ { areaId: current[0].AreaId, name: payload.name, groupId }
885
+ );
886
+ if (duplicate[0]?.cnt > 0) {
887
+ throw new Error(
888
+ `Another Scheduling Group with name '${payload.name}' already exists in this Area`
889
+ );
890
+ }
891
+ }
892
+ updates.push(`Name = @name`);
893
+ params.name = payload.name;
894
+ }
895
+
896
+ if (payload.allocationsMenu !== undefined) {
897
+ updates.push(`AllocationsMenu = @allocationsMenu`);
898
+ params.allocationsMenu = payload.allocationsMenu;
899
+ }
900
+
901
+ if (payload.notes !== undefined) {
902
+ updates.push(`Notes = @notes`);
903
+ params.notes = payload.notes;
904
+ }
905
+
906
+ if (updates.length > 0) {
907
+ updates.push(`LastAmendedBy = @userId`);
908
+ updates.push(`LastAmendedDate = GETDATE()`);
909
+
910
+ await dbClient.query(
911
+ `UPDATE SchedulingGroup SET ${updates.join(", ")} WHERE Id = @groupId`,
912
+ params
913
+ );
914
+ logger.info("SchedulingGroup updated", { groupId, correlationId });
915
+ }
916
+
917
+ // Update teams if provided
918
+ if (payload.teams !== undefined && payload.teams.length >= 0) {
919
+ // Remove old team mappings
920
+ await dbClient.query(
921
+ `DELETE FROM SchedulingGroupTeam WHERE SchedulingGroupId = @groupId`,
922
+ { groupId }
923
+ );
924
+ // Add new team mappings
925
+ for (const teamId of payload.teams) {
926
+ await dbClient.query(
927
+ `INSERT INTO SchedulingGroupTeam (SchedulingGroupId, TeamId) VALUES (@groupId, @teamId)`,
928
+ { groupId, teamId }
929
+ );
930
+ }
931
+ logger.info("Teams reassigned to SchedulingGroup", {
932
+ groupId,
933
+ teamCount: payload.teams.length,
934
+ correlationId,
935
+ });
936
+ }
937
+
938
+ // Create audit history record for update
939
+ const updated = await dbClient.query(
940
+ `SELECT AreaId, Name, AllocationsMenu, Notes FROM SchedulingGroup WHERE Id = @groupId`,
941
+ { groupId }
942
+ );
943
+
944
+ await dbClient.query(
945
+ `INSERT INTO SchedulingGroupHistory (SchedulingGroupId, AreaId, Name, AllocationsMenu, Notes, LastAmendedBy, LastAmendedDate, Operation, ChangeDetails)
946
+ VALUES (@groupId, @areaId, @name, @allocationsMenu, @notes, @userId, GETDATE(), 'UPDATE', @changes)`,
947
+ {
948
+ groupId,
949
+ areaId: updated[0].AreaId,
950
+ name: updated[0].Name,
951
+ allocationsMenu: updated[0].AllocationsMenu,
952
+ notes: updated[0].Notes,
953
+ userId,
954
+ changes: JSON.stringify({ before: current[0], after: updated[0] }),
955
+ }
956
+ );
957
+ logger.info("Audit history record created for update", { groupId, correlationId });
958
+ } catch (error) {
959
+ logger.error("Failed to update SchedulingGroup stub", { error, correlationId });
960
+ throw error;
961
+ }
962
+ },
963
+
964
+ // DELETE: Remove Scheduling Group with cascade cleanup
965
+ deleteSchedulingGroupStub: async (
966
+ groupId: number,
967
+ userId: string,
968
+ correlationId?: string
969
+ ): Promise<void> => {
970
+ try {
971
+ logger.info("Deleting SchedulingGroup stub from database", {
972
+ groupId,
973
+ userId,
974
+ correlationId,
975
+ });
976
+
977
+ // Verify group exists
978
+ const group = await dbClient.query(
979
+ `SELECT Id, Name FROM SchedulingGroup WHERE Id = @groupId`,
980
+ { groupId }
981
+ );
982
+ if (!group.length) {
983
+ throw new Error(`SchedulingGroup with ID ${groupId} not found`);
984
+ }
985
+
986
+ // Create audit history record for deletion (soft audit trail)
987
+ await dbClient.query(
988
+ `INSERT INTO SchedulingGroupHistory (SchedulingGroupId, AreaId, Name, AllocationsMenu, Notes, LastAmendedBy, LastAmendedDate, Operation)
989
+ SELECT Id, AreaId, Name, AllocationsMenu, Notes, @userId, GETDATE(), 'DELETE'
990
+ FROM SchedulingGroup WHERE Id = @groupId`,
991
+ { groupId, userId }
992
+ );
993
+ logger.info("Delete audit history record created", { groupId, correlationId });
994
+
995
+ // Delete team mappings (cascade)
996
+ await dbClient.query(
997
+ `DELETE FROM SchedulingGroupTeam WHERE SchedulingGroupId = @groupId`,
998
+ { groupId }
999
+ );
1000
+ logger.info("Associated teams unlinked", { groupId, correlationId });
1001
+
1002
+ // Delete the group
1003
+ const deleteResult = await dbClient.query(
1004
+ `DELETE FROM SchedulingGroup WHERE Id = @groupId`,
1005
+ { groupId }
1006
+ );
1007
+
1008
+ logger.info("SchedulingGroup deleted successfully", {
1009
+ groupId,
1010
+ groupName: group[0].Name,
1011
+ correlationId,
1012
+ });
1013
+ } catch (error) {
1014
+ logger.error("Failed to delete SchedulingGroup stub", { error, correlationId });
1015
+ throw error;
1016
+ }
1017
+ },
1018
+
1019
+ // Helper: Get list of groups for an area with permission check
1020
+ getGroupsForArea: async (
1021
+ areaId: number,
1022
+ userId: string,
1023
+ userRole: 'SYSTEM_ADMIN' | 'AREA_ADMIN' | 'VIEW_ONLY',
1024
+ correlationId?: string
1025
+ ): Promise<any[]> => {
1026
+ try {
1027
+ // Permission check: AREA_ADMIN can only see their areas, SYSTEM_ADMIN sees all
1028
+ if (userRole === 'AREA_ADMIN') {
1029
+ const hasPermission = await dbClient.query(
1030
+ `SELECT COUNT(*) as cnt FROM AreaPermission WHERE UserId = @userId AND AreaId = @areaId AND PermissionType = 'AREA_ADMIN'`,
1031
+ { userId, areaId }
1032
+ );
1033
+ if (!hasPermission[0]?.cnt) {
1034
+ throw new Error(`Permission denied: Area Admin access to area ${areaId} required`);
1035
+ }
1036
+ }
1037
+
1038
+ const groups = await dbClient.query(
1039
+ `SELECT
1040
+ sg.Id,
1041
+ sg.AreaId,
1042
+ a.Name as AreaName,
1043
+ sg.Name,
1044
+ sg.AllocationsMenu,
1045
+ sg.Notes,
1046
+ sg.LastAmendedDate,
1047
+ sg.LastAmendedBy,
1048
+ STRING_AGG(CAST(sgt.TeamId AS VARCHAR), ',') as AssociatedTeamIds
1049
+ FROM SchedulingGroup sg
1050
+ JOIN Area a ON sg.AreaId = a.Id
1051
+ LEFT JOIN SchedulingGroupTeam sgt ON sg.Id = sgt.SchedulingGroupId
1052
+ WHERE sg.AreaId = @areaId
1053
+ GROUP BY sg.Id, sg.AreaId, a.Name, sg.Name, sg.AllocationsMenu, sg.Notes, sg.LastAmendedDate, sg.LastAmendedBy
1054
+ ORDER BY sg.LastAmendedDate DESC`,
1055
+ { areaId }
1056
+ );
1057
+
1058
+ logger.info("Retrieved SchedulingGroups for area", {
1059
+ areaId,
1060
+ groupCount: groups.length,
1061
+ correlationId,
1062
+ });
1063
+ return groups;
1064
+ } catch (error) {
1065
+ logger.error("Failed to get groups for area", { error, correlationId });
1066
+ throw error;
1067
+ }
1068
+ },
1069
+
1070
+ // Helper: Get history for a group
1071
+ getGroupHistory: async (groupId: number, correlationId?: string): Promise<any[]> => {
1072
+ try {
1073
+ const history = await dbClient.query(
1074
+ `SELECT
1075
+ Id,
1076
+ SchedulingGroupId,
1077
+ AreaId,
1078
+ Name,
1079
+ AllocationsMenu,
1080
+ Notes,
1081
+ LastAmendedBy,
1082
+ LastAmendedDate,
1083
+ Operation,
1084
+ ChangeDetails
1085
+ FROM SchedulingGroupHistory
1086
+ WHERE SchedulingGroupId = @groupId
1087
+ ORDER BY LastAmendedDate DESC`,
1088
+ { groupId }
1089
+ );
1090
+
1091
+ logger.info("Retrieved SchedulingGroup history", {
1092
+ groupId,
1093
+ recordCount: history.length,
1094
+ correlationId,
1095
+ });
1096
+ return history;
1097
+ } catch (error) {
1098
+ logger.error("Failed to get group history", { error, correlationId });
1099
+ throw error;
1100
+ }
1101
+ },
1102
+ };
1103
+ ```
1104
+
1105
+ ## Test Implementation
1106
+
1107
+ ### 6-Phase Testing Strategy for Scheduling Groups
1108
+
1109
+ #### Phase 0 – Preparation (T0)
1110
+
1111
+ **Objectives:**
1112
+ - Confirm test scope (happy path + permission variants)
1113
+ - Capture all business rules and constraints
1114
+ - Define seam strategy (DB stubs until API ready)
1115
+ - Document user impersonation approach
1116
+
1117
+ **Deliverables:**
1118
+ 1. **Test Scope Document:**
1119
+ - Happy Path: Create Scheduling Group (System Admin, Area Admin perspectives)
1120
+ - Permission Variants: System Admin (allowed all areas), Area Admin (area-restricted), View Only (denied)
1121
+ - Edge Cases: Duplicate names, invalid areas, missing team mappings
1122
+
1123
+ 2. **Business Rules Checklist:**
1124
+ - Area auto-fill from user's Area Admin permission
1125
+ - Allocations Menu default behavior (currently TBD, document assumption)
1126
+ - History tracks: name, area, teams, allocationsMenu, notes, lastAmendedBy, lastAmendedDate
1127
+ - Delete confirmation message includes group name
1128
+ - Editable vs read-only fields per role
1129
+
1130
+ 3. **Seam Strategy:**
1131
+ - Until PHP API endpoints exist: Use DB-driven stored procedures or direct inserts
1132
+ - Implement API abstraction layer supporting mock/real swap
1133
+ - Document which table operations trigger audit (SchedulingGroupHistory)
1134
+ - Decide: transaction-per-test (fast rollback) vs truncate+reseed per-suite
1135
+
1136
+ #### Phase 1 – Test Environment Bootstrapping (T1)
1137
+
1138
+ **Objectives:**
1139
+ - Create deterministic seed dataset with fixed IDs
1140
+ - Establish user impersonation mechanism
1141
+ - Document dependency order
1142
+
1143
+ **Seed Dataset Structure:**
1144
+ ```sql
1145
+ -- Stable test data with fixed IDs for reproducibility
1146
+ INSERT INTO Area (Id, Name) VALUES
1147
+ (10001, 'Test Area - North'),
1148
+ (10002, 'Test Area - South');
1149
+
1150
+ INSERT INTO SchedulingTeam (Id, Name, AreaId) VALUES
1151
+ (20001, 'Morning Team', 10001),
1152
+ (20002, 'Afternoon Team', 10001),
1153
+ (20003, 'Evening Team', 10002),
1154
+ (20004, 'Night Team', 10002);
1155
+
1156
+ -- Test users with roles (seeded before test run)
1157
+ INSERT INTO [User] (Id, Name, Email, Role) VALUES
1158
+ (30001, 'SystemAdmin Test', 'sysadmin@test.local', 'SYSTEM_ADMIN'),
1159
+ (30002, 'AreaAdmin North', 'areaadmin.north@test.local', 'AREA_ADMIN'),
1160
+ (30003, 'AreaAdmin South', 'areaadmin.south@test.local', 'AREA_ADMIN'),
1161
+ (30004, 'View Only User', 'readonly@test.local', 'VIEW_ONLY');
1162
+
1163
+ -- Area permissions for Area Admins
1164
+ INSERT INTO AreaPermission (UserId, AreaId, PermissionType) VALUES
1165
+ (30002, 10001, 'AREA_ADMIN'),
1166
+ (30003, 10002, 'AREA_ADMIN');
1167
+
1168
+ -- Optional: Pre-existing Scheduling Groups for list/edit/delete tests
1169
+ INSERT INTO SchedulingGroup (Id, AreaId, Name, AllocationsMenu, Notes, LastAmendedBy, LastAmendedDate) VALUES
1170
+ (40001, 10001, 'Morning Group - North', 1, 'Groups morning teams in North Area', 30001, GETDATE()),
1171
+ (40002, 10002, 'Evening Group - South', 0, 'Groups evening teams in South Area', 30001, GETDATE());
1172
+ ```
1173
+
1174
+ **User Impersonation Mechanism:**
1175
+ - **Primary:** x-test-user-id header (API requests, test-env only)
1176
+ - **Fallback:** Seeded DB roles with transaction isolation per test
1177
+ - **UI E2E:** SSO bypass for test users (configured in test container)
1178
+
1179
+ **Dependency Order:**
1180
+ 1. Areas created first (base entities)
1181
+ 2. SchedulingTeams created with AreaId FK
1182
+ 3. Users created
1183
+ 4. AreaPermissions assigned
1184
+ 5. Optional pre-existing SchedulingGroups inserted
1185
+
1186
+ #### Phase 2 – API Abstraction & Interim Approach (T1.2 / T3.1)
1187
+
1188
+ **Objectives:**
1189
+ - Define API client interface
1190
+ - Provide DB-driven stubs until real endpoints exist
1191
+ - Support correlation tracking and test headers
1192
+
1193
+ **API Abstraction Layer:**
1194
+ ```typescript
1195
+ // Until PHP API ready: interface + mock + DB stub implementation
1196
+ interface ISchedulingGroupClient {
1197
+ create(payload: CreateSchedulingGroupDTO, userId: string): Promise<CreateResponse>;
1198
+ getById(id: number, userId: string): Promise<SchedulingGroupDTO>;
1199
+ update(id: number, payload: UpdateSchedulingGroupDTO, userId: string): Promise<void>;
1200
+ delete(id: number, userId: string): Promise<void>;
1201
+ list(areaId: number, userId: string): Promise<SchedulingGroupDTO[]>;
1202
+ getHistory(groupId: number, userId: string): Promise<HistoryRecord[]>;
1203
+ }
1204
+
1205
+ // Implementation: DB-driven (now) → API-driven (when endpoints ready)
1206
+ class SchedulingGroupApiClient implements ISchedulingGroupClient {
1207
+ constructor(private mode: 'db-stub' | 'api', private dbClient: any, private httpClient: any) {}
1208
+
1209
+ async create(payload: CreateSchedulingGroupDTO, userId: string): Promise<CreateResponse> {
1210
+ const correlationId = utils.getCorrelationId();
1211
+ logger.info('Creating SchedulingGroup', { payload, userId, correlationId });
1212
+
1213
+ if (this.mode === 'db-stub') {
1214
+ // Call stored procedure or direct insert (validates DB behavior now)
1215
+ return await schedulingGroupMutations.createSchedulingGroupStub(payload, userId);
1216
+ } else {
1217
+ // Call real API endpoint (when available)
1218
+ return await this.httpClient.post('/scheduling-groups', payload, {
1219
+ headers: {
1220
+ 'x-correlation-id': correlationId,
1221
+ 'x-test-user-id': userId
1222
+ }
1223
+ });
1224
+ }
1225
+ }
1226
+
1227
+ // ... other CRUD methods follow same pattern
1228
+ }
1229
+ ```
1230
+
1231
+ **Header Support:**
1232
+ - **x-correlation-id**: UUID generated per test for request tracing
1233
+ - **x-test-user-id**: User ID for permission validation in test-env (removed in prod)
1234
+
1235
+ #### Phase 3 – Seed/Reset Automation (T2)
1236
+
1237
+ **Objectives:**
1238
+ - Automate deterministic test data setup
1239
+ - Implement fast rollback mechanism
1240
+ - Document affected tables and cleanup order
1241
+
1242
+ **Cleanup Order (Rollback Safe):**
1243
+ ```typescript
1244
+ export const dbCleanup = {
1245
+ resetSchedulingGroupTables: async () => {
1246
+ // Order matters: delete children before parents
1247
+ await dbClient.query(`DELETE FROM SchedulingGroupHistory`);
1248
+ await dbClient.query(`DELETE FROM SchedulingGroupTeam`);
1249
+ await dbClient.query(`DELETE FROM SchedulingGroup`);
1250
+ // Keep Areas, Teams, Users, Permissions for next test
1251
+ },
1252
+
1253
+ fullReset: async () => {
1254
+ // Only in isolated test suites; use resetSchedulingGroupTables in normal flows
1255
+ await dbClient.query(`DELETE FROM SchedulingGroupHistory`);
1256
+ await dbClient.query(`DELETE FROM SchedulingGroupTeam`);
1257
+ await dbClient.query(`DELETE FROM SchedulingGroup`);
1258
+ await dbClient.query(`DELETE FROM AreaPermission`);
1259
+ await dbClient.query(`DELETE FROM [User]`);
1260
+ await dbClient.query(`DELETE FROM SchedulingTeam`);
1261
+ await dbClient.query(`DELETE FROM Area`);
1262
+ },
1263
+
1264
+ reseedTestData: async () => {
1265
+ // Re-insert deterministic seed (IDs 10001+, 20001+, etc.)
1266
+ await seedRunner.seed();
1267
+ }
1268
+ };
1269
+
1270
+ // Pre-test hook
1271
+ Before(async function (this: SchedulingGroupWorld) {
1272
+ await dbCleanup.resetSchedulingGroupTables();
1273
+ await dbCleanup.reseedTestData();
1274
+ this.context = contextProvider.createTestContext();
1275
+ });
1276
+
1277
+ // Post-test hook
1278
+ After(async function (this: SchedulingGroupWorld) {
1279
+ if (this.groupId) {
1280
+ await schedulingGroupMutations.deleteSchedulingGroupStub(this.groupId);
1281
+ }
1282
+ // Transaction will auto-rollback if test failed, else commit
1283
+ });
1284
+ ```
1285
+
1286
+ **Affected Tables:**
1287
+ - `SchedulingGroup` (main table)
1288
+ - `SchedulingGroupTeam` (many-to-many junction)
1289
+ - `SchedulingGroupHistory` (audit trail)
1290
+ - Potentially: `Area`, `SchedulingTeam`, `User` (seeded once, reused)
1291
+ - Potentially: `AreaPermission` (for permission tests)
1292
+
1293
+ #### Phase 4 – Test Suites & Coverage (T3)
1294
+
1295
+ **Objectives:**
1296
+ - Build DB-driven smoke tests (now, API pending)
1297
+ - Build permission matrix tests (parameterised)
1298
+ - Build UI E2E thin-client tests
1299
+ - Implement concurrency and negative test cases
1300
+
1301
+ **4a. DB-Driven Smoke Test Suite:**
1302
+ ```typescript
1303
+ Feature: Scheduling Group Creation - DB Smoke Tests
1304
+
1305
+ Background:
1306
+ Given the test database is seeded with Areas, Teams, and Users
1307
+ And correlation tracking is enabled
1308
+
1309
+ Scenario: System Admin creates Scheduling Group (happy path)
1310
+ Given System Admin user is available
1311
+ And a valid Scheduling Group payload is prepared with teams [20001, 20002]
1312
+ When the user creates the Scheduling Group via DB stub
1313
+ Then the Scheduling Group should exist in database with correct ID
1314
+ And the allocationsMenu field should default to [TBD: true|false|null]
1315
+ And the history record should capture create event with correct lastAmendedBy
1316
+ And the associated teams should be linked in SchedulingGroupTeam junction table
1317
+ And correlation ID should be logged in audit trail
1318
+
1319
+ Scenario: Area Admin creates Scheduling Group for their Area (permission check)
1320
+ Given Area Admin (North) user is available
1321
+ And Area Admin is assigned to Area [10001]
1322
+ And a valid Scheduling Group payload is prepared for Area 10001
1323
+ When the user creates the Scheduling Group via DB stub
1324
+ Then the Scheduling Group should exist with AreaId = 10001
1325
+ And lastAmendedBy should reflect the Area Admin's ID
1326
+ And history should show Area Admin as the creator
1327
+
1328
+ Scenario: Area Admin cannot create group outside assigned Area (permission denied)
1329
+ Given Area Admin (North) user is available (assigned to Area 10001)
1330
+ And a valid Scheduling Group payload is prepared for Area 10002 (South)
1331
+ When the user attempts to create the Scheduling Group via DB stub
1332
+ Then the create operation should fail with [PermissionDenied | ForeignKeyViolation]
1333
+ And no row should be inserted in SchedulingGroup table
1334
+ And no row should be inserted in SchedulingGroupHistory
1335
+
1336
+ Scenario: Create with duplicate Scheduling Group name in same Area (business rule validation)
1337
+ Given System Admin user is available
1338
+ And a Scheduling Group with name [test-name] already exists in Area 10001
1339
+ And a valid Scheduling Group payload with the same name for Area 10001
1340
+ When the user creates the Scheduling Group via DB stub
1341
+ Then the create operation should fail with [UniquenessViolation | BusinessRuleViolation]
1342
+ And the error message should indicate duplicate name
1343
+ And no new row should be inserted
1344
+
1345
+ Scenario: Create with invalid or missing team IDs (referential integrity)
1346
+ Given System Admin user is available
1347
+ And a valid Scheduling Group payload with non-existent team IDs [99999, 88888]
1348
+ When the user creates the Scheduling Group via DB stub
1349
+ Then the create operation should fail with [ForeignKeyViolation]
1350
+ And no row should be inserted in SchedulingGroupTeam
1351
+
1352
+ Scenario: Edit (Update) existing Scheduling Group
1353
+ Given System Admin user is available
1354
+ And an existing Scheduling Group exists with name [old-name]
1355
+ And a valid update payload with new name [new-name] and different teams
1356
+ When the user updates the Scheduling Group via DB stub
1357
+ Then the database record should reflect new name [new-name]
1358
+ And the allocationsMenu field should be updated if provided
1359
+ And the Notes field should be updated
1360
+ And a new history record should be inserted capturing the change
1361
+ And history should show lastAmendedBy and new lastAmendedDate
1362
+
1363
+ Scenario: Delete existing Scheduling Group
1364
+ Given System Admin user is available
1365
+ And an existing Scheduling Group with ID [40001] exists
1366
+ When the user deletes the Scheduling Group via DB stub
1367
+ Then the row should be removed from SchedulingGroup table
1368
+ And all associated rows in SchedulingGroupTeam should be removed (cascade)
1369
+ And a history record may be retained for audit (TBD: soft delete vs hard delete)
1370
+ And the operation should complete without error
1371
+
1372
+ Scenario: View (Get) list of Scheduling Groups for an Area
1373
+ Given a System Admin user is available
1374
+ And multiple Scheduling Groups exist in Areas [10001, 10002]
1375
+ When the user retrieves the list for Area 10001 via DB query
1376
+ Then the result should include only groups where AreaId = 10001
1377
+ And each result should include column data: name, allocationsMenu, notes, teams, lastAmendedDate, lastAmendedBy
1378
+ And results should be ordered as specified (or default order documented)
1379
+
1380
+ Scenario: View history of changes to a Scheduling Group
1381
+ Given System Admin user is available
1382
+ And a Scheduling Group with multiple historical changes exists
1383
+ When the user retrieves the history for this group
1384
+ Then all history records should be returned in order (newest first)
1385
+ And each record should include: old values (before), new values (after), timestamp, user who changed
1386
+ And column headings should match SchedulingGroup table structure
1387
+ ```
1388
+
1389
+ **4b. Permission Matrix Test Suite (Parameterised):**
1390
+ ```typescript
1391
+ Feature: Scheduling Group Permission Validation
1392
+
1393
+ Scenario Outline: Permission matrix for CRUD operations
1394
+ Given a user with role [<USER_ROLE>]
1395
+ And user is assigned to Area [<USER_AREA>]
1396
+ And a Scheduling Group exists in Area [<GROUP_AREA>]
1397
+ When the user attempts to [<ACTION>] the Scheduling Group
1398
+ Then the operation result should be [<EXPECTED_RESULT>]
1399
+
1400
+ Examples:
1401
+ | USER_ROLE | USER_AREA | GROUP_AREA | ACTION | EXPECTED_RESULT |
1402
+ | SYSTEM_ADMIN | ANY | 10001 | CREATE | ALLOW |
1403
+ | SYSTEM_ADMIN | ANY | 10001 | READ | ALLOW |
1404
+ | SYSTEM_ADMIN | ANY | 10001 | UPDATE | ALLOW |
1405
+ | SYSTEM_ADMIN | ANY | 10001 | DELETE | ALLOW |
1406
+ | AREA_ADMIN | 10001 | 10001 | CREATE | ALLOW |
1407
+ | AREA_ADMIN | 10001 | 10001 | READ | ALLOW |
1408
+ | AREA_ADMIN | 10001 | 10001 | UPDATE | ALLOW |
1409
+ | AREA_ADMIN | 10001 | 10001 | DELETE | ALLOW |
1410
+ | AREA_ADMIN | 10001 | 10002 | CREATE | DENY (Permission) |
1411
+ | AREA_ADMIN | 10001 | 10002 | READ | DENY (Permission) |
1412
+ | AREA_ADMIN | 10001 | 10002 | UPDATE | DENY (Permission) |
1413
+ | AREA_ADMIN | 10001 | 10002 | DELETE | DENY (Permission) |
1414
+ | VIEW_ONLY | ANY | ANY | CREATE | DENY (Role) |
1415
+ | VIEW_ONLY | ANY | ANY | READ | ALLOW (List only) |
1416
+ | VIEW_ONLY | ANY | ANY | UPDATE | DENY (Role) |
1417
+ | VIEW_ONLY | ANY | ANY | DELETE | DENY (Role) |
1418
+ ```
1419
+
1420
+ **4c. UI E2E Thin-Client Tests:**
1421
+ ```typescript
1422
+ Feature: Scheduling Group UI E2E - Fast Path
1423
+
1424
+ Scenario: Area Admin logs in and creates Scheduling Group via UI
1425
+ Given an Area Admin user with permissions for Area [10001]
1426
+ When the user navigates to Admin > Admins > Scheduling Groups
1427
+ Then the screen should display list of groups for their Area
1428
+ And the "Add" button should be visible
1429
+ When the user clicks "Add" and submits valid group details
1430
+ Then the new group should appear in the grid
1431
+ And the database should contain the new group with correct history
1432
+ And correlation ID from UI request should match DB audit trail
1433
+
1434
+ Scenario: View group history modal opens with audit records
1435
+ Given Area Admin is on Scheduling Groups list
1436
+ And a group with multiple historical changes exists
1437
+ When the user clicks the "History" icon for that group
1438
+ Then a modal should show all amendments: columns, old→new values, timestamp, who
1439
+ And records should be ordered newest first
1440
+ And modal should close without side effects
1441
+ ```
1442
+
1443
+ **4d. Negative & Concurrency Tests:**
1444
+ ```typescript
1445
+ Feature: Scheduling Group Negative & Concurrency
1446
+
1447
+ Scenario: Concurrent creates with same name (race condition)
1448
+ Given two test sessions as System Admin
1449
+ And both prepare identical group names for Area [10001]
1450
+ When both sessions attempt create simultaneously
1451
+ Then one should succeed, one should fail with UniquenessViolation
1452
+ And only one row should exist in SchedulingGroup table
1453
+ And history should show only one create event
1454
+
1455
+ Scenario: Delete while history is being queried (read consistency)
1456
+ Given a group with history records exists
1457
+ When one session deletes the group and another queries its history
1458
+ Then both operations should complete without locks or deadlocks
1459
+ And history should remain queryable (soft delete or cascading logic)
1460
+ ```
1461
+
1462
+ #### Phase 5 – Observability & Correlation (T1.1 / T4)
1463
+
1464
+ **Objectives:**
1465
+ - Generate and track correlation IDs
1466
+ - Implement structured logging
1467
+ - Provide test output for fast triage
1468
+
1469
+ **Correlation ID Flow:**
1470
+ ```typescript
1471
+ export const utils = {
1472
+ getCorrelationId: (): string => uuidv4(),
1473
+
1474
+ logWithCorrelation: (correlationId: string, level: 'info'|'error'|'warn', message: string, data?: any) => {
1475
+ // Structured log with correlation context
1476
+ console.log(JSON.stringify({
1477
+ timestamp: new Date().toISOString(),
1478
+ correlationId,
1479
+ level,
1480
+ message,
1481
+ ...data
1482
+ }));
1483
+ }
1484
+ };
1485
+
1486
+ // In test
1487
+ Before(async function (this: SchedulingGroupWorld) {
1488
+ this.context.correlationId = utils.getCorrelationId();
1489
+ logger.info(`Test started with correlation ID: ${this.context.correlationId}`);
1490
+ });
1491
+
1492
+ // When creating group
1493
+ When('the user creates...', async function (this: SchedulingGroupWorld) {
1494
+ utils.logWithCorrelation(
1495
+ this.context.correlationId,
1496
+ 'info',
1497
+ 'Attempting SchedulingGroup create',
1498
+ { userId: this.user.id, areaId: this.context.payload.areaId }
1499
+ );
1500
+ const result = await apiClient.create(this.context.payload, this.user.id);
1501
+ utils.logWithCorrelation(
1502
+ this.context.correlationId,
1503
+ 'info',
1504
+ 'SchedulingGroup created',
1505
+ { groupId: result.id, dbRow: await dbClient.query(...) }
1506
+ );
1507
+ });
1508
+ ```
1509
+
1510
+ #### Phase 6 – CI & Release (T4)
1511
+
1512
+ **Objectives:**
1513
+ - Define CI job flow
1514
+ - Establish gateways
1515
+ - Publish artifacts
1516
+
1517
+ **CI Job Flow:**
1518
+ ```yaml
1519
+ # Example GitHub Actions or similar
1520
+ name: Scheduling Group Tests
1521
+
1522
+ on: [push, pull_request]
1523
+
1524
+ jobs:
1525
+ tests:
1526
+ runs-on: ubuntu-latest
1527
+ services:
1528
+ mssql:
1529
+ image: mcr.microsoft.com/mssql/server:2019-latest
1530
+ env:
1531
+ SA_PASSWORD: TestPassword123!
1532
+ ACCEPT_EULA: Y
1533
+ options: >-
1534
+ --health-cmd="/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P TestPassword123! -Q 'SELECT 1'"
1535
+ --health-interval 10s
1536
+ --health-timeout 5s
1537
+ --health-retries 3
1538
+
1539
+ steps:
1540
+ - uses: actions/checkout@v3
1541
+
1542
+ - name: Setup Node
1543
+ uses: actions/setup-node@v3
1544
+ with:
1545
+ node-version: '16'
1546
+
1547
+ - name: Install dependencies
1548
+ run: npm install --legacy-peer-deps
1549
+
1550
+ - name: Reset test database
1551
+ run: npm run reset-db
1552
+ env:
1553
+ DB_HOST: localhost
1554
+ DB_PORT: 1433
1555
+ DB_USER: sa
1556
+ DB_PASSWORD: TestPassword123!
1557
+
1558
+ - name: Seed test data
1559
+ run: npm run seed-db
1560
+ env:
1561
+ DB_HOST: localhost
1562
+ DB_PORT: 1433
1563
+ DB_USER: sa
1564
+ DB_PASSWORD: TestPassword123!
1565
+
1566
+ - name: Run API (DB-stub) tests
1567
+ run: npm run bdd -- --tags @critical
1568
+ env:
1569
+ DB_HOST: localhost
1570
+ DB_PORT: 1433
1571
+ DB_USER: sa
1572
+ DB_PASSWORD: TestPassword123!
1573
+
1574
+ - name: Run UI smoke tests (optional)
1575
+ run: npm run test:ui -- --project=ui
1576
+ if: success()
1577
+
1578
+ - name: Publish test reports
1579
+ uses: actions/upload-artifact@v3
1580
+ if: always()
1581
+ with:
1582
+ name: test-reports
1583
+ path: reports/
1584
+
1585
+ - name: Publish Playwright report
1586
+ uses: actions/upload-artifact@v3
1587
+ if: always()
1588
+ with:
1589
+ name: playwright-report
1590
+ path: reports/html/
1591
+ ```
1592
+
1593
+ **Gateway Checks:**
1594
+ - ✅ All Phase 4 test suites pass (DB smoke, permission matrix, negative cases)
1595
+ - ✅ No destructive DB operations run against non-test environments
1596
+ - ✅ Correlation IDs present in logs for all operations
1597
+ - ✅ Test reports show repeatability (3 runs, same result)
1598
+ - ✅ Code review on test implementations and seed scripts
1599
+
1600
+ ---
1601
+
1602
+ ### Playwright BDD Integration Setup – `configs/global-setup.ts`
1603
+
1604
+ ```typescript
1605
+ import { chromium, FullConfig } from "@playwright/test";
1606
+
1607
+ async function globalSetup(config: FullConfig) {
1608
+ console.log("Running global setup for Playwright BDD...");
1609
+
1610
+ // Initialize browser instance for BDD if needed
1611
+ const browser = await chromium.launch();
1612
+ await browser.close();
1613
+
1614
+ console.log("Global setup completed");
1615
+ }
1616
+
1617
+ export default globalSetup;
1618
+ ```
1619
+
1620
+ ### Playwright BDD Concept
1621
+
1622
+ Playwright BDD (Behavior-Driven Development) integrates Gherkin feature files with Playwright tests. This allows writing human-readable test scenarios that map to actual test code.
1623
+
1624
+ **Key Benefits:**
1625
+ - **Readability**: Feature files are written in plain English using Gherkin syntax
1626
+ - **Collaboration**: Non-technical stakeholders can understand test scenarios
1627
+ - **Reusability**: Step definitions are reusable across multiple scenarios
1628
+ - **Maintainability**: Changes to logic only need to be made in step definitions
1629
+ - **Traceability**: Clear mapping between business requirements and test code
1630
+
1631
+ **BDD Workflow:**
1632
+ 1. Write Gherkin feature files describing business scenarios
1633
+ 2. Define step implementations that map to Gherkin steps
1634
+ 3. Use `Before` and `After` hooks to manage fixtures and context lifecycle
1635
+ 4. Steps access shared context (TestContext) injected through BDD World
1636
+ 5. Steps interact with test clients (DB, API, UI) to execute actions
1637
+ 6. Assertions verify expected outcomes
1638
+
1639
+ **Setting Up BDD with Playwright using bddgen:**
1640
+
1641
+ The framework uses `bddgen` to automatically generate BDD step definitions from feature files. Install dependencies:
1642
+
1643
+ ```bash
1644
+ npm install --save-dev @cucumber/cucumber bddgen @cucumber/pretty-formatter
1645
+ ```
1646
+
1647
+ Create a `bddgen.config.js` configuration file in the project root:
1648
+
1649
+ ```javascript
1650
+ // bddgen.config.js
1651
+ module.exports = {
1652
+ features: 'src/modules/**/*.feature',
1653
+ steps: 'src/modules/**/steps',
1654
+ language: 'typescript',
1655
+ typescript: true,
1656
+ requireModule: ['ts-node/register'],
1657
+ formatters: {
1658
+ progress: true,
1659
+ html: 'reports/cucumber-report.html',
1660
+ json: 'reports/cucumber-report.json'
1661
+ }
1662
+ };
1663
+ ```
1664
+
1665
+ Create a `cucumber.js` configuration file in the project root:
1666
+
1667
+ ```javascript
1668
+ // cucumber.js
1669
+ module.exports = {
1670
+ default: {
1671
+ require: ['src/modules/**/*.steps.ts'],
1672
+ requireModule: ['ts-node/register'],
1673
+ format: ['progress', 'html:reports/cucumber-report.html', 'json:reports/cucumber-report.json'],
1674
+ formatOptions: {
1675
+ snippetInterface: 'async-await'
1676
+ }
1677
+ }
1678
+ };
1679
+ ```
1680
+
1681
+ **BDD Generation Workflow:**
1682
+
1683
+ When running tests, `bddgen` automatically:
1684
+ 1. Scans feature files in `src/modules/**/*.feature`
1685
+ 2. Generates corresponding step definitions in `src/modules/**/steps`
1686
+ 3. Creates TypeScript step stubs based on Gherkin syntax
1687
+ 4. Integrates with Playwright test configuration
1688
+
1689
+ No need to manually write step stubs - `bddgen` creates them automatically from your feature files.
1690
+
1691
+ **Key BDD Concepts in This Framework:**
1692
+
1693
+ - **World Interface**: Shared context across all steps in a scenario
1694
+ - **Before/After Hooks**: Initialize fixtures and cleanup resources
1695
+ - **Fixture Injection**: Access contextProvider to setup users and test data
1696
+ - **Scenario Isolation**: Each scenario gets fresh context and cleanup
1697
+ - **Automatic Step Generation**: bddgen automatically creates step definitions from feature files
1698
+ - **Test Pipeline**: Feature → bddgen → Step Definitions → Cucumber.js → Test Execution
1699
+
1700
+ ### BDD Generation with bddgen – Workflow
1701
+
1702
+ **What is bddgen?**
1703
+
1704
+ `bddgen` is a command-line tool that automatically generates TypeScript step definitions from Gherkin feature files. Instead of manually writing step stubs, bddgen scans your `.feature` files and creates matching step definitions in TypeScript.
1705
+
1706
+ **Workflow:**
1707
+
1708
+ ```
1709
+ 1. Create Feature Files
1710
+ └─ src/modules/scheduling-group/features/create-scheduling-group.feature
1711
+
1712
+ 2. Run: npm run generate:bdd
1713
+ └─ bddgen scans feature files for Given/When/Then steps
1714
+
1715
+ 3. Auto-generated Step Files
1716
+ └─ src/modules/scheduling-group/steps/createSchedulingGroup.steps.ts
1717
+ (with Before/After hooks, World interface, step stubs)
1718
+
1719
+ 4. Implement Step Logic
1720
+ └─ Add fixture calls, API/DB operations
1721
+ └─ Add assertions in Then steps
1722
+
1723
+ 5. Run: npm run bdd
1724
+ └─ cucumber-js executes generated steps from feature files
1725
+ └─ BDD reports generated to reports/ directory
1726
+ ```
1727
+
1728
+ **Configuration – `bddgen.config.js`:**
1729
+
1730
+ ```javascript
1731
+ module.exports = {
1732
+ features: 'src/modules/**/*.feature', // Where to find feature files
1733
+ steps: 'src/modules/**/steps', // Where to generate step files
1734
+ language: 'typescript', // Output language
1735
+ typescript: true, // Use TypeScript syntax
1736
+ requireModule: ['ts-node/register'], // Enable TS support
1737
+ formatters: {
1738
+ progress: true, // Show progress in console
1739
+ html: 'reports/cucumber-report.html', // HTML report location
1740
+ json: 'reports/cucumber-report.json' // JSON report location
1741
+ }
1742
+ };
1743
+ ```
1744
+
1745
+ **Generated Step Structure:**
1746
+
1747
+ When bddgen processes your feature file, it automatically creates:
1748
+
1749
+ ```typescript
1750
+ // Auto-generated with Before/After hooks
1751
+ Before(async function (this: SchedulingGroupWorld) {
1752
+ // Initialization code (you implement this)
1753
+ });
1754
+
1755
+ After(async function (this: SchedulingGroupWorld) {
1756
+ // Cleanup code (you implement this)
1757
+ });
1758
+
1759
+ // Auto-generated step stubs from feature file
1760
+ Given("an Area Admin user is available", async function (this: SchedulingGroupWorld) {
1761
+ // TODO: Implement this step
1762
+ });
1763
+
1764
+ When("the user creates the Scheduling Group", async function (this: SchedulingGroupWorld) {
1765
+ // TODO: Implement this step
1766
+ });
1767
+
1768
+ Then("the result should be successful", async function (this: SchedulingGroupWorld) {
1769
+ // TODO: Implement this step
1770
+ });
1771
+ ```
1772
+
1773
+ **Advantages of bddgen:**
1774
+
1775
+ ✅ **Automatic**: No manual step stub creation
1776
+ ✅ **Fast**: Generates from feature files in seconds
1777
+ ✅ **Type-Safe**: Creates TypeScript types from World interfaces
1778
+ ✅ **Synchronized**: Steps always match feature file structure
1779
+ ✅ **DRY**: Single source of truth in feature files
1780
+
1781
+ **npm Scripts with bddgen:**
1782
+
1783
+ ```bash
1784
+ # Only generate BDD steps
1785
+ npm run generate:bdd
1786
+
1787
+ # Generate + Run BDD tests
1788
+ npm run bdd
1789
+
1790
+ # Generate + Run with watch mode
1791
+ npm run bdd:watch
1792
+
1793
+ # Generate + Run all tests
1794
+ npm test
1795
+
1796
+ # Generate + Run with debug
1797
+ npm run test:debug
1798
+ ```
1799
+
1800
+ **Important:** All test commands automatically call `npm run generate:bdd` before execution, ensuring your step definitions are always up-to-date with your feature files.
1801
+
1802
+ ### Step Definitions with Fixture Integration – `modules/scheduling-group/steps/createSchedulingGroup.steps.ts`
1803
+
1804
+ ```typescript
1805
+ import { Given, When, Then, Before, After, setDefaultTimeout } from "@cucumber/cucumber";
1806
+ import { Browser, BrowserContext, Page } from "@playwright/test";
1807
+ import { expect } from "@playwright/test";
1808
+ import { wrapperClient } from "@test-harness/wrapperClient";
1809
+ import { dbClient } from "@test-harness/dbClient";
1810
+ import { schedulingGroupMutations } from "../queries/schedule.mutations";
1811
+ import { schedulingGroupRead } from "../read-models/schedule.read";
1812
+ import { schedulingGroupBuilder } from "../test-data/schedule.seed";
1813
+ import { contextProvider, TestContext } from "@fixtures/context.fixture";
1814
+ import { utils } from "@utils/correlation";
1815
+ import { logger } from "@utils/logger";
1816
+
1817
+ setDefaultTimeout(60 * 1000);
1818
+
1819
+ // World interface for BDD context
1820
+ interface SchedulingGroupWorld {
1821
+ context: TestContext;
1822
+ user: any;
1823
+ groupId?: number;
1824
+ page?: Page;
1825
+ browser?: Browser;
1826
+ browserContext?: BrowserContext;
1827
+ }
1828
+
1829
+ // Initialize context before each scenario
1830
+ Before(async function (this: SchedulingGroupWorld) {
1831
+ logger.info("Setting up test context for scenario");
1832
+
1833
+ this.context = contextProvider.createTestContext();
1834
+ this.user = await contextProvider.getCurrentUser();
1835
+ this.groupId = undefined;
1836
+
1837
+ logger.info("Test context initialized", { user: this.user });
1838
+ });
1839
+
1840
+ // Cleanup after each scenario
1841
+ After(async function (this: SchedulingGroupWorld) {
1842
+ logger.info("Cleaning up test context");
1843
+
1844
+ if (this.groupId) {
1845
+ try {
1846
+ await schedulingGroupMutations.deleteSchedulingGroupStub(this.groupId);
1847
+ logger.info("Cleaned up created resources", { groupId: this.groupId });
1848
+ } catch (error) {
1849
+ logger.warn("Cleanup failed", error);
1850
+ }
1851
+ }
1852
+
1853
+ if (this.page) {
1854
+ await this.page.close();
1855
+ }
1856
+
1857
+ if (this.browserContext) {
1858
+ await this.browserContext.close();
1859
+ }
1860
+ });
1861
+
1862
+ // Step definitions with fixture injection
1863
+ Given("an Area Admin user is available", async function (this: SchedulingGroupWorld) {
1864
+ this.user = await contextProvider.getAreaAdminUser();
1865
+ this.context.user = this.user;
1866
+
1867
+ logger.info("Area Admin user set up", { user: this.user });
1868
+ });
1869
+
1870
+ Given("a regular user is available", async function (this: SchedulingGroupWorld) {
1871
+ this.user = await contextProvider.getRegularUser();
1872
+ this.context.user = this.user;
1873
+
1874
+ logger.info("Regular user set up", { user: this.user });
1875
+ });
1876
+
1877
+ Given("a valid Scheduling Group payload is prepared", async function (this: SchedulingGroupWorld) {
1878
+ this.context.payload = schedulingGroupBuilder.createValid();
1879
+
1880
+ utils.log("Payload prepared", this.context.payload);
1881
+ });
1882
+
1883
+ Given("a minimal Scheduling Group payload is prepared", async function (this: SchedulingGroupWorld) {
1884
+ this.context.payload = schedulingGroupBuilder.createMinimal();
1885
+
1886
+ utils.log("Minimal payload prepared", this.context.payload);
1887
+ });
1888
+
1889
+ When("the user creates the Scheduling Group via DB stub", async function (this: SchedulingGroupWorld) {
1890
+ expect(this.context.payload).toBeDefined();
1891
+ expect(this.user).toBeDefined();
1892
+
1893
+ this.groupId = await schedulingGroupMutations.createSchedulingGroupStub(
1894
+ this.context.payload,
1895
+ this.user.id
1896
+ );
1897
+
1898
+ this.context.groupId = this.groupId;
1899
+ logger.info("Scheduling Group created via DB stub", { groupId: this.groupId });
1900
+ });
1901
+
1902
+ When("the user creates the Scheduling Group via API", async function (this: SchedulingGroupWorld) {
1903
+ expect(this.context.payload).toBeDefined();
1904
+ expect(this.user).toBeDefined();
1905
+
1906
+ this.context.response = await wrapperClient.createSchedulingGroup(
1907
+ this.context.payload,
1908
+ this.user
1909
+ );
1910
+
1911
+ this.groupId = this.context.response?.id;
1912
+ logger.info("Scheduling Group created via API", { groupId: this.groupId });
1913
+ });
1914
+
1915
+ Then("the Scheduling Group should exist in the database", async function (this: SchedulingGroupWorld) {
1916
+ expect(this.context.payload).toBeDefined();
1917
+ expect(this.groupId).toBeDefined();
1918
+
1919
+ const record = await schedulingGroupRead.getById(this.groupId!);
1920
+
1921
+ expect(record).toBeDefined();
1922
+ expect(record.name).toBe(this.context.payload.name);
1923
+ });
1924
+
1925
+ Then("the Scheduling Group should exist with correct invariants", async function (this: SchedulingGroupWorld) {
1926
+ expect(this.context.payload).toBeDefined();
1927
+ expect(this.groupId).toBeDefined();
1928
+
1929
+ const record = await schedulingGroupRead.getByName(this.context.payload.name);
1930
+ const normalized = utils.normalizeTimestamps(record);
1931
+
1932
+ expect(normalized).not.toBeNull();
1933
+ expect(normalized.areaId).toBe(this.context.payload.areaId);
1934
+ expect(normalized.allocationsMenu).toBe(this.context.payload.allocationsMenu);
1935
+ expect(normalized.notes).toBe(this.context.payload.notes);
1936
+
1937
+ const teams = await schedulingGroupRead.getTeamsByGroupId(this.groupId!);
1938
+ expect(teams.length).toBe(this.context.payload.teams.length);
1939
+ expect(teams.map((t: any) => t.TeamId)).toEqual(this.context.payload.teams);
1940
+
1941
+ const history = await schedulingGroupRead.getHistoryByGroupId(this.groupId!);
1942
+ expect(history.length).toBeGreaterThan(0);
1943
+ expect(history[0].LastAmendedBy).toBe(this.user.id);
1944
+ });
1945
+
1946
+ Then("the {string} should have {int} teams", async function (this: SchedulingGroupWorld, resource: string, expectedCount: number) {
1947
+ expect(this.groupId).toBeDefined();
1948
+
1949
+ const teams = await schedulingGroupRead.getTeamsByGroupId(this.groupId!);
1950
+ expect(teams.length).toBe(expectedCount);
1951
+ });
1952
+ ```
1953
+
1954
+ ### Feature File – `modules/scheduling-group/features/scheduling-group-crud.feature`
1955
+
1956
+ ```gherkin
1957
+ Feature: Scheduling Group CRUD Operations - Full Business Coverage
1958
+
1959
+ Background:
1960
+ # Pre-seeded deterministic test data with fixed IDs
1961
+ Given the test database is seeded with:
1962
+ | Areas | AreaId | AreaName |
1963
+ | 10001 | Test Area - North |
1964
+ | 10002 | Test Area - South |
1965
+ | Teams | TeamId | TeamName | AreaId |
1966
+ | 20001 | Morning Team | 10001 |
1967
+ | 20002 | Afternoon Team | 10001 |
1968
+ | 20003 | Evening Team | 10002 |
1969
+ | Users | UserId | Role |
1970
+ | 30001 | SYSTEM_ADMIN |
1971
+ | 30002 | AREA_ADMIN (North) |
1972
+ | 30003 | AREA_ADMIN (South) |
1973
+ | 30004 | VIEW_ONLY |
1974
+ And correlation tracking is enabled
1975
+
1976
+ # ========== PHASE 4A: DB-DRIVEN SMOKE TESTS ==========
1977
+
1978
+ Scenario: NP035.02 - System Admin creates new Scheduling Group (happy path)
1979
+ Given System Admin user (30001) is available
1980
+ And Area is set to 10001 (Test Area - North)
1981
+ And a valid Scheduling Group payload with:
1982
+ | Field | Value |
1983
+ | Name | Morning Shift Grp |
1984
+ | AllocationsMenu | true |
1985
+ | Notes | Groups morning teams for North |
1986
+ | Teams | [20001, 20002] |
1987
+ When the user creates the Scheduling Group via DB stub
1988
+ Then the operation should succeed with returned groupId
1989
+ And the SchedulingGroup table should contain:
1990
+ | Column | Expected Value |
1991
+ | AreaId | 10001 |
1992
+ | Name | Morning Shift Grp |
1993
+ | AllocationsMenu | 1 (true) |
1994
+ | Notes | Groups morning teams for North |
1995
+ | LastAmendedBy | 30001 |
1996
+ And the SchedulingGroupTeam table should link teams [20001, 20002]
1997
+ And the SchedulingGroupHistory should record:
1998
+ | Operation | Name | AreaId | LastAmendedBy |
1999
+ | CREATE | Morning Shift Grp | 10001 | 30001 |
2000
+ And correlation ID should be captured in log entry
2001
+
2002
+ Scenario: NP035.02 - Area Admin creates Scheduling Group for assigned Area
2003
+ Given Area Admin user (30002) is available and assigned to Area 10001
2004
+ And a valid Scheduling Group payload with:
2005
+ | Field | Value |
2006
+ | Name | Evening Shift |
2007
+ | AllocationsMenu | false |
2008
+ | Notes | Evening coverage |
2009
+ | Teams | [20001] |
2010
+ When the user creates the Scheduling Group via DB stub
2011
+ Then the operation should succeed
2012
+ And the SchedulingGroup should show LastAmendedBy = 30002 (Area Admin user ID)
2013
+ And the SchedulingGroupHistory should record Area Admin (30002) as creator
2014
+
2015
+ Scenario: NP035.02 - Create with duplicate name should fail (business rule)
2016
+ Given System Admin user (30001) is available
2017
+ And a Scheduling Group with name "Duplicate Test" already exists in Area 10001
2018
+ And a valid Scheduling Group payload with same name "Duplicate Test" for Area 10001
2019
+ When the user attempts to create the Scheduling Group via DB stub
2020
+ Then the operation should fail with error message containing "already exists"
2021
+ And no row should be inserted in SchedulingGroup table
2022
+ And no row should be inserted in SchedulingGroupHistory
2023
+ And the database should remain in consistent state
2024
+
2025
+ Scenario: NP035.02 - Create with invalid/missing team IDs should fail (referential integrity)
2026
+ Given System Admin user (30001) is available
2027
+ And a valid Scheduling Group payload with non-existent team IDs [99999, 88888]
2028
+ When the user attempts to create the Scheduling Group via DB stub
2029
+ Then the operation should fail with ForeignKeyViolation error
2030
+ And no row should be inserted in SchedulingGroup table
2031
+
2032
+ Scenario: NP035.02 - Create with invalid Area should fail (referential integrity)
2033
+ Given System Admin user (30001) is available
2034
+ And a valid Scheduling Group payload for non-existent Area 99999
2035
+ When the user attempts to create the Scheduling Group via DB stub
2036
+ Then the operation should fail with error "Area does not exist"
2037
+ And no row should be inserted in SchedulingGroup table
2038
+
2039
+ Scenario: NP035.03 - Area Admin edits Scheduling Group in assigned Area
2040
+ Given Area Admin user (30002) is available and assigned to Area 10001
2041
+ And an existing Scheduling Group with ID 40001 in Area 10001
2042
+ And an update payload with:
2043
+ | Field | New Value |
2044
+ | Name | Updated Group |
2045
+ | AllocationsMenu | true |
2046
+ | Notes | Updated notes |
2047
+ | Teams | [20002] |
2048
+ When the user updates the Scheduling Group via DB stub
2049
+ Then the database record should reflect:
2050
+ | Column | Value |
2051
+ | Name | Updated Group |
2052
+ | AllocationsMenu | 1 |
2053
+ | Notes | Updated notes |
2054
+ | LastAmendedBy | 30002 |
2055
+ And the SchedulingGroupTeam should now link only [20002]
2056
+ And SchedulingGroupHistory should record UPDATE operation with before/after values
2057
+
2058
+ Scenario: NP035.04 - System Admin deletes Scheduling Group (including cascade cleanup)
2059
+ Given System Admin user (30001) is available
2060
+ And an existing Scheduling Group with ID 40001 and team links [20001, 20002]
2061
+ When the user deletes the Scheduling Group via DB stub
2062
+ Then the SchedulingGroup table should not contain row with ID 40001
2063
+ And the SchedulingGroupTeam table should not contain rows for groupId 40001 (cascade delete)
2064
+ And the SchedulingGroupHistory should retain audit record showing DELETE operation
2065
+ And LastAmendedBy in history should show 30001 (System Admin user)
2066
+
2067
+ Scenario: NP035.01 - View list of Scheduling Groups for an Area
2068
+ Given System Admin user (30001) is available
2069
+ And multiple Scheduling Groups exist:
2070
+ | GroupId | AreaId | Name | AllocationsMenu | Teams |
2071
+ | 40001 | 10001 | Morning Shift | 1 | [20001] |
2072
+ | 40002 | 10001 | Afternoon Shift | 0 | [20002] |
2073
+ | 40003 | 10002 | Evening Shift - South | 1 | [20003] |
2074
+ When the user retrieves groups for Area 10001 via DB query
2075
+ Then the result should include only groups with AreaId = 10001: [40001, 40002]
2076
+ And each result should contain all columns:
2077
+ - AreaId, Name, AllocationsMenu, Notes, AssociatedTeamIds, LastAmendedDate, LastAmendedBy
2078
+ And groups should be ordered by LastAmendedDate DESC (newest first)
2079
+
2080
+ Scenario: NP035.05 - View history of changes to a Scheduling Group
2081
+ Given System Admin user (30001) is available
2082
+ And a Scheduling Group with multiple historical changes:
2083
+ | Timestamp | Operation | ChangeDetails | User |
2084
+ | T1 | CREATE | Initial creation | 30001 |
2085
+ | T2 | UPDATE | Name changed | 30002 |
2086
+ | T3 | UPDATE | AllocationsMenu toggled| 30001 |
2087
+ When the user retrieves history for this group
2088
+ Then history should contain all records in DESC order (newest first):
2089
+ | Timestamp | Operation | User |
2090
+ | T3 | UPDATE | 30001 |
2091
+ | T2 | UPDATE | 30002 |
2092
+ | T1 | CREATE | 30001 |
2093
+ And each record should include: SchedulingGroupId, AreaId, Name, AllocationsMenu, Notes, LastAmendedBy, LastAmendedDate, Operation
2094
+
2095
+ # ========== PHASE 4B: PERMISSION MATRIX TESTS ==========
2096
+
2097
+ Scenario Outline: Permission matrix - CRUD access by user role and area assignment
2098
+ Given a user with role [<USER_ROLE>]
2099
+ And user is currently assigned to Area [<USER_AREA>]
2100
+ And a Scheduling Group exists in Area [<GROUP_AREA>]
2101
+ When the user attempts to [<ACTION>] the Scheduling Group
2102
+ Then the operation result should be [<EXPECTED_RESULT>]
2103
+ And if denied, the database state should remain unchanged (no side effects)
2104
+
2105
+ Examples:
2106
+ | USER_ROLE | USER_AREA | GROUP_AREA | ACTION | EXPECTED_RESULT |
2107
+ | SYSTEM_ADMIN| ANY | 10001 | CREATE | ALLOW |
2108
+ | SYSTEM_ADMIN| ANY | 10001 | READ | ALLOW |
2109
+ | SYSTEM_ADMIN| ANY | 10001 | UPDATE | ALLOW |
2110
+ | SYSTEM_ADMIN| ANY | 10001 | DELETE | ALLOW |
2111
+ | SYSTEM_ADMIN| ANY | 10002 | CREATE | ALLOW |
2112
+ | SYSTEM_ADMIN| ANY | 10002 | DELETE | ALLOW |
2113
+ | AREA_ADMIN | 10001 | 10001 | CREATE | ALLOW |
2114
+ | AREA_ADMIN | 10001 | 10001 | READ | ALLOW |
2115
+ | AREA_ADMIN | 10001 | 10001 | UPDATE | ALLOW |
2116
+ | AREA_ADMIN | 10001 | 10001 | DELETE | ALLOW |
2117
+ | AREA_ADMIN | 10001 | 10002 | CREATE | DENY (Permission) |
2118
+ | AREA_ADMIN | 10001 | 10002 | READ | DENY (Permission) |
2119
+ | AREA_ADMIN | 10001 | 10002 | UPDATE | DENY (Permission) |
2120
+ | AREA_ADMIN | 10001 | 10002 | DELETE | DENY (Permission) |
2121
+ | VIEW_ONLY | ANY | ANY | CREATE | DENY (Role) |
2122
+ | VIEW_ONLY | ANY | ANY | UPDATE | DENY (Role) |
2123
+ | VIEW_ONLY | ANY | ANY | DELETE | DENY (Role) |
2124
+ | VIEW_ONLY | ANY | ANY | READ | ALLOW |
2125
+
2126
+ # ========== PHASE 4C: UI E2E TESTS ==========
2127
+
2128
+ Scenario: UI E2E - Area Admin navigates to Scheduling Groups screen and creates group
2129
+ Given an Area Admin user (30002) authenticated for Area 10001
2130
+ When the user navigates to Admin > Admins > Scheduling Groups
2131
+ Then the screen should load successfully
2132
+ And the user should be presented with:
2133
+ - List of existing Scheduling Groups for Area 10001
2134
+ - Column headers: Actions, Area, Scheduling Group Name, Associated Scheduling Teams, Allocations Menu, Notes, Last Amended Date, Last Amended By
2135
+ - "Add" button (top-right or right-click context menu)
2136
+ When the user clicks "Add" button
2137
+ Then an Add dialog should open with fields:
2138
+ - Area (auto-filled as 10001, non-editable)
2139
+ - Scheduling Group Name (text input, editable)
2140
+ - Associated Scheduling Teams (multi-select dropdown)
2141
+ - Allocations Menu (toggle yes/no, default TBD)
2142
+ - Notes (text area, optional)
2143
+ When the user fills in and submits:
2144
+ | Field | Value |
2145
+ | Scheduling Group Name | New Test Grp |
2146
+ | Associated Scheduling Teams| Morning Team |
2147
+ | Allocations Menu | Yes |
2148
+ | Notes | Test note |
2149
+ Then the dialog should close
2150
+ And the new group should appear in the grid immediately
2151
+ And database verification:
2152
+ - SchedulingGroup table contains row with Name = "New Test Grp"
2153
+ - SchedulingGroupTeam links the Morning Team
2154
+ - SchedulingGroupHistory captures CREATE operation
2155
+ - Correlation ID from UI request matches DB audit trail
2156
+
2157
+ Scenario: UI E2E - View history modal shows all amendments
2158
+ Given Area Admin is on Scheduling Groups list
2159
+ And a group with multiple historical changes exists
2160
+ When the user clicks the "History" icon for that group
2161
+ Then a history modal should open displaying:
2162
+ | Column | Content |
2163
+ | Timestamp | ISO datetime |
2164
+ | Operation | CREATE / UPDATE / DELETE |
2165
+ | User Changed | Full name + user ID |
2166
+ | Change Details | Old values → New values |
2167
+ And records should be ordered newest first
2168
+ When the user closes the modal
2169
+ Then focus should return to the grid without side effects
2170
+
2171
+ # ========== PHASE 4D: NEGATIVE & CONCURRENCY TESTS ==========
2172
+
2173
+ Scenario: Concurrency - Two creates with identical name (race condition)
2174
+ Given two test sessions both as System Admin (30001)
2175
+ And both sessions prepare identical group:
2176
+ | Field | Value |
2177
+ | AreaId | 10001 |
2178
+ | Name | Concurrent Test |
2179
+ | Teams | [20001, 20002] |
2180
+ When both sessions attempt create simultaneously
2181
+ Then one should succeed with returned groupId
2182
+ And one should fail with error "already exists"
2183
+ And database should contain exactly one row with Name = "Concurrent Test"
2184
+ And SchedulingGroupHistory should record only one CREATE event
2185
+
2186
+ Scenario: Delete while history is being queried (read consistency)
2187
+ Given a group with ID 40005 and history records exists
2188
+ When one session executes DELETE and another queries history concurrently
2189
+ Then both operations should complete without deadlock / timeout
2190
+ And history should remain queryable post-delete (audit trail preserved)
2191
+ And group should be removed from active SchedulingGroup table
2192
+ ```
2193
+
2194
+ ## Database Scripts
2195
+
2196
+ ### Cucumber Configuration – `cucumber.js`
2197
+
2198
+ ```javascript
2199
+ module.exports = {
2200
+ default: {
2201
+ require: ['src/modules/**/*.steps.ts'],
2202
+ requireModule: ['ts-node/register'],
2203
+ format: [
2204
+ 'progress',
2205
+ 'html:reports/cucumber-report.html',
2206
+ 'json:reports/cucumber-report.json'
2207
+ ],
2208
+ formatOptions: {
2209
+ snippetInterface: 'async-await'
2210
+ },
2211
+ parallel: 2,
2212
+ retry: 0,
2213
+ dryRun: false
2214
+ }
2215
+ };
2216
+ ```
2217
+
2218
+ ## Database Scripts
2219
+
2220
+ ### Seed Script – `scripts/seed-db.ts`
2221
+
2222
+ ```typescript
2223
+ import { dbClient } from "../src/core/test-harness/dbClient";
2224
+
2225
+ async function seed() {
2226
+ await dbClient.query(`INSERT INTO Area(Id, Name) VALUES (101, 'London')`);
2227
+ await dbClient.query(`
2228
+ INSERT INTO SchedulingTeam(Id, Name, AreaId)
2229
+ VALUES (201,'TeamA',101), (202,'TeamB',101)
2230
+ `);
2231
+ console.log("DB seeded successfully");
2232
+ }
2233
+
2234
+ seed();
2235
+ ```
2236
+
2237
+ ### Reset Script – `scripts/reset-db.ts`
2238
+
2239
+ ```typescript
2240
+ import { dbClient } from "../src/core/test-harness/dbClient";
2241
+
2242
+ async function reset() {
2243
+ await dbClient.query(`TRUNCATE TABLE SchedulingGroup`);
2244
+ await dbClient.query(`TRUNCATE TABLE SchedulingGroupTeam`);
2245
+ await dbClient.query(`TRUNCATE TABLE SchedulingGroupHistory`);
2246
+ console.log("DB reset successfully");
2247
+ }
2248
+
2249
+ reset();
2250
+ ```
2251
+
2252
+ ## Package Configuration
2253
+
2254
+ ### package.json
2255
+
2256
+ ```json
2257
+ {
2258
+ "name": "automation-framework",
2259
+ "version": "1.0.0",
2260
+ "description": "Playwright TypeScript Test Automation Framework",
2261
+ "scripts": {
2262
+ "seed-db": "ts-node scripts/seed-db.ts",
2263
+ "reset-db": "ts-node scripts/reset-db.ts",
2264
+ "apitest": "playwright test --project=api",
2265
+ "uitest": "playwright test --project=ui",
2266
+ "dbtest": "playwright test --project=db",
2267
+ "test": "playwright test --project=integrated",
2268
+ "test:debug": "playwright test --debug",
2269
+ "test:ui": "playwright test --ui",
2270
+ "pretest": "npm run reset-db && npm run seed-db"
2271
+ },
2272
+ "dependencies": {
2273
+ "axios": "^1.7.0",
2274
+ "dotenv": "^16.4.1",
2275
+ "mssql": "^11.0.0",
2276
+ "uuid": "^9.0.1"
2277
+ },
2278
+ "devDependencies": {
2279
+ "@cucumber/cucumber": "^10.0.1",
2280
+ "@playwright/test": "^1.47.0",
2281
+ "@types/node": "^20.15.0",
2282
+ "@types/uuid": "^9.0.8",
2283
+ "bddgen": "^1.0.0",
2284
+ "playwright": "^1.47.0",
2285
+ "ts-node": "^10.9.2",
2286
+ "typescript": "^5.6.2"
2287
+ }
2288
+ }
2289
+ ```
2290
+
2291
+ ### Dependencies Overview
2292
+
2293
+ #### Production Dependencies
2294
+ - **axios** (^1.7.0) - HTTP client for API requests
2295
+ - **dotenv** (^16.4.1) - Environment variable management
2296
+ - **mssql** (^11.0.0) - MSSQL database connection driver
2297
+ - **uuid** (^9.0.1) - UUID generation for correlation IDs
2298
+
2299
+ #### Development Dependencies
2300
+ - **@playwright/test** (^1.47.0) - Playwright testing framework
2301
+ - **@types/node** (^20.15.0) - TypeScript types for Node.js
2302
+ - **@types/uuid** (^9.0.8) - TypeScript types for UUID
2303
+ - **@cucumber/cucumber** (^10.0.1) - BDD framework for Gherkin integration
2304
+ - **bddgen** (^1.0.0) - **⭐ Automatic BDD step generation from feature files**
2305
+ - **playwright** (^1.47.0) - Playwright browser automation
2306
+ - **ts-node** (^10.9.2) - TypeScript execution engine
2307
+ - **typescript** (^5.6.2) - TypeScript compiler
2308
+
2309
+ #### What is bddgen?
2310
+ `bddgen` automates the creation of TypeScript step definitions from Gherkin feature files. When you run test commands, bddgen:
2311
+ 1. Scans your `.feature` files
2312
+ 2. Extracts Given/When/Then steps
2313
+ 3. Generates TypeScript step definition files with proper types
2314
+ 4. Creates Before/After hooks and World interface
2315
+ 5. Eliminates manual step stub creation
2316
+
2317
+ ### package.json Scripts
2318
+
2319
+ ```json
2320
+ {
2321
+ "scripts": {
2322
+ "seed-db": "ts-node scripts/seed-db.ts",
2323
+ "reset-db": "ts-node scripts/reset-db.ts",
2324
+ "generate:bdd": "bddgen",
2325
+ "apitest": "playwright test --project=api",
2326
+ "uitest": "playwright test --project=ui",
2327
+ "dbtest": "playwright test --project=db",
2328
+ "test": "npm run generate:bdd && playwright test --project=integrated",
2329
+ "test:debug": "npm run generate:bdd && playwright test --debug",
2330
+ "test:ui": "npm run generate:bdd && playwright test --ui",
2331
+ "bdd": "npm run generate:bdd && cucumber-js",
2332
+ "bdd:report": "npm run generate:bdd && cucumber-js --format json:reports/cucumber-report.json",
2333
+ "bdd:watch": "npm run generate:bdd && cucumber-js --watch",
2334
+ "pretest": "npm run reset-db && npm run seed-db"
2335
+ }
2336
+ }
2337
+ ```
2338
+
2339
+ ## Running Tests
2340
+
2341
+ ### Complete BDD Testing Workflow with bddgen
2342
+
2343
+ **Step-by-Step Execution Flow:**
2344
+
2345
+ ```
2346
+ ┌─────────────────────────────────────────────────────────────────┐
2347
+ │ 1. SETUP PHASE │
2348
+ │ └─ npm install --legacy-peer-deps │
2349
+ │ └─ npm run reset-db │
2350
+ │ └─ npm run seed-db │
2351
+ └─────────────────────────────────────────────────────────────────┘
2352
+
2353
+ ┌─────────────────────────────────────────────────────────────────┐
2354
+ │ 2. FEATURE FILE CREATION │
2355
+ │ └─ Write .feature files in src/modules/**/features/ │
2356
+ │ └─ Define Given/When/Then scenarios in Gherkin syntax │
2357
+ └─────────────────────────────────────────────────────────────────┘
2358
+
2359
+ ┌─────────────────────────────────────────────────────────────────┐
2360
+ │ 3. BDD GENERATION (Automatic or Manual) │
2361
+ │ └─ npm run generate:bdd │
2362
+ │ └─ bddgen scans .feature files │
2363
+ │ └─ Generates .steps.ts files with step stubs │
2364
+ │ └─ Creates World interface and Before/After hooks │
2365
+ └─────────────────────────────────────────────────────────────────┘
2366
+
2367
+ ┌─────────────────────────────────────────────────────────────────┐
2368
+ │ 4. STEP IMPLEMENTATION │
2369
+ │ └─ Implement generated step functions │
2370
+ │ └─ Add fixture calls (contextProvider) │
2371
+ │ └─ Add API/DB operations │
2372
+ │ └─ Add assertions in Then steps │
2373
+ └─────────────────────────────────────────────────────────────────┘
2374
+
2375
+ ┌─────────────────────────────────────────────────────────────────┐
2376
+ │ 5. TEST EXECUTION │
2377
+ │ └─ npm run bdd (automatic bddgen) │
2378
+ │ └─ cucumber-js reads .feature files │
2379
+ │ └─ Executes matching step definitions │
2380
+ │ └─ Runs Before/After hooks for setup/cleanup │
2381
+ │ └─ Injects World context into step functions │
2382
+ └─────────────────────────────────────────────────────────────────┘
2383
+
2384
+ ┌─────────────────────────────────────────────────────────────────┐
2385
+ │ 6. REPORT GENERATION │
2386
+ │ └─ HTML report: reports/cucumber-report.html │
2387
+ │ └─ JSON report: reports/cucumber-report.json │
2388
+ │ └─ Progress output in console │
2389
+ └─────────────────────────────────────────────────────────────────┘
2390
+ ```
2391
+
2392
+ **Typical Test Session:**
2393
+
2394
+ ```bash
2395
+ # 1. Install and setup
2396
+ npm install --legacy-peer-deps
2397
+ npm run reset-db && npm run seed-db
2398
+
2399
+ # 2. Run tests (bddgen runs automatically)
2400
+ npm run bdd
2401
+
2402
+ # 3. View reports
2403
+ open reports/cucumber-report.html
2404
+
2405
+ # 4. Watch mode for development
2406
+ npm run bdd:watch
2407
+ ```
2408
+
2409
+ ### Installation & Setup
2410
+
2411
+ 1. **Install Dependencies:**
2412
+ ```bash
2413
+ npm install
2414
+ ```
2415
+
2416
+ If you encounter peer dependency warnings or conflicts, use:
2417
+ ```bash
2418
+ npm install --legacy-peer-deps
2419
+ ```
2420
+
2421
+ For a clean installation:
2422
+ ```bash
2423
+ rm -r node_modules package-lock.json
2424
+ npm install
2425
+ ```
2426
+
2427
+ 2. **Verify Installation:**
2428
+ ```bash
2429
+ npm list
2430
+ ```
2431
+
2432
+ 3. **Configure Environment:**
2433
+ - Copy or update `.env` file with your database and API credentials
2434
+ - Ensure all required services (Database, API) are accessible
2435
+
2436
+ 4. **Reset the database:**
2437
+ ```bash
2438
+ npm run reset-db
2439
+ ```
2440
+
2441
+ 5. **Seed the database:**
2442
+ ```bash
2443
+ npm run seed-db
2444
+ ```
2445
+
2446
+ 6. **Run Tests:**
2447
+
2448
+ Generate BDD tests from feature files:
2449
+ ```bash
2450
+ npm run generate:bdd
2451
+ ```
2452
+
2453
+ Run all integrated tests (auto-generates BDD):
2454
+ ```bash
2455
+ npm test
2456
+ ```
2457
+
2458
+ Run BDD feature tests (auto-generates BDD):
2459
+ ```bash
2460
+ npm run bdd
2461
+ ```
2462
+
2463
+ Run BDD tests with watch mode (auto-generates BDD):
2464
+ ```bash
2465
+ npm run bdd:watch
2466
+ ```
2467
+
2468
+ Run API tests only:
2469
+ ```bash
2470
+ npm run apitest
2471
+ ```
2472
+
2473
+ Run UI tests only:
2474
+ ```bash
2475
+ npm run uitest
2476
+ ```
2477
+
2478
+ Run DB tests only:
2479
+ ```bash
2480
+ npm run dbtest
2481
+ ```
2482
+
2483
+ Debug tests (auto-generates BDD):
2484
+ ```bash
2485
+ npm run test:debug
2486
+ ```
2487
+
2488
+ Run tests with UI mode (auto-generates BDD):
2489
+ ```bash
2490
+ npm run test:ui
2491
+ ```
2492
+
2493
+ 7. **View Test Reports:**
2494
+ Check the `reports/` directory for generated reports:
2495
+ - `reports/html/index.html` - Interactive HTML report
2496
+ - `reports/results.json` - JSON test results
2497
+ - `reports/junit.xml` - JUnit XML format for CI/CD
2498
+
2499
+ ### Troubleshooting Dependency Installation
2500
+
2501
+ **Issue: Peer dependency conflicts**
2502
+ ```bash
2503
+ npm install --legacy-peer-deps
2504
+ ```
2505
+
2506
+ **Issue: bddgen not found or failing**
2507
+ - Ensure bddgen is installed: `npm install --save-dev bddgen`
2508
+ - Verify `bddgen.config.js` exists in project root
2509
+ - Check that feature files exist in `src/modules/**/*.feature`
2510
+ - Verify `steps` directory exists: `src/modules/**/steps`
2511
+ - Clear generated files and regenerate: `npm run generate:bdd`
2512
+
2513
+ **Issue: MSSQL connection errors**
2514
+ - Ensure MSSQL Server is running and accessible
2515
+ - Verify credentials in `.env` file
2516
+ - Check firewall settings
2517
+
2518
+ **Issue: Playwright browser installation**
2519
+ ```bash
2520
+ npx playwright install
2521
+ ```
2522
+
2523
+ **Issue: TypeScript compilation errors**
2524
+ ```bash
2525
+ npm run test -- --config playwright.config.ts
2526
+ ```
2527
+
2528
+ **Issue: Module not found errors**
2529
+ - Ensure `baseUrl` and `paths` are correctly set in `tsconfig.json`
2530
+ - Clear TypeScript cache: `rm -rf dist/`
2531
+ - Reinstall dependencies: `npm install --legacy-peer-deps`
2532
+
2533
+ **Issue: Generated step files not found**
2534
+ - Run `npm run generate:bdd` explicitly before tests
2535
+ - Verify bddgen output directory matches `cucumber.js` require path
2536
+ - Check that feature files have matching step pattern names
2537
+
2538
+ ### Node & NPM Version Requirements
2539
+
2540
+ - **Node.js**: >= 16.x
2541
+ - **NPM**: >= 8.x
2542
+ - **TypeScript**: 5.6.2
2543
+ - **Playwright**: 1.47.0
2544
+
2545
+ Check your versions:
2546
+ ```bash
2547
+ node --version
2548
+ npm --version
2549
+ ```
2550
+
2551
+ ## BDD Best Practices
2552
+
2553
+ ### Using Fixtures Effectively in BDD
2554
+
2555
+ 1. **Initialize Fixtures in Before Hooks:**
2556
+ ```typescript
2557
+ Before(async function (this: YourWorld) {
2558
+ this.context = contextProvider.createTestContext();
2559
+ this.user = await contextProvider.getCurrentUser();
2560
+ });
2561
+ ```
2562
+
2563
+ 2. **Access Fixtures in Given Steps:**
2564
+ ```typescript
2565
+ Given("a specific user type", async function (this: YourWorld) {
2566
+ this.user = await contextProvider.getAreaAdminUser();
2567
+ this.context.user = this.user;
2568
+ });
2569
+ ```
2570
+
2571
+ 3. **Use Fixtures in When/Then Steps:**
2572
+ ```typescript
2573
+ When("the user performs action", async function (this: YourWorld) {
2574
+ // Access prepared fixture data
2575
+ await action(this.context.payload, this.user);
2576
+ });
2577
+ ```
2578
+
2579
+ 4. **Cleanup Resources in After Hooks:**
2580
+ ```typescript
2581
+ After(async function (this: YourWorld) {
2582
+ if (this.groupId) {
2583
+ await schedulingGroupMutations.deleteSchedulingGroupStub(this.groupId);
2584
+ }
2585
+ });
2586
+ ```
2587
+
2588
+ ### Writing Reusable BDD Steps
2589
+
2590
+ - **Use parameterized steps** for flexibility:
2591
+ ```typescript
2592
+ Then("the {string} should have {int} items", async function (resource, count) {
2593
+ // Implementation
2594
+ });
2595
+ ```
2596
+
2597
+ - **Leverage fixtures for setup** instead of hardcoding values:
2598
+ ```typescript
2599
+ Given("a valid {string} payload", async function (resourceType) {
2600
+ // Use builder based on resourceType
2601
+ });
2602
+ ```
2603
+
2604
+ - **Separate concerns** between fixtures and steps:
2605
+ - Fixtures: Create test data, setup users, initialize context
2606
+ - Steps: Execute actions, make assertions
2607
+
2608
+ ### Avoiding Common BDD Pitfalls
2609
+
2610
+ ❌ **Don't:** Hardcode test data in steps
2611
+ ```typescript
2612
+ // Bad
2613
+ const payload = { name: "HardcodedName", areaId: 101 };
2614
+ ```
2615
+
2616
+ ✅ **Do:** Use builders from fixtures
2617
+ ```typescript
2618
+ // Good
2619
+ const payload = schedulingGroupBuilder.createValid();
2620
+ ```
2621
+
2622
+ ❌ **Don't:** Skip cleanup in After hooks
2623
+ ```typescript
2624
+ // Bad - resources leak between scenarios
2625
+ After(async function () {
2626
+ // Empty cleanup
2627
+ });
2628
+ ```
2629
+
2630
+ ✅ **Do:** Always cleanup created resources
2631
+ ```typescript
2632
+ // Good
2633
+ After(async function (this: YourWorld) {
2634
+ if (this.groupId) {
2635
+ await schedulingGroupMutations.deleteSchedulingGroupStub(this.groupId);
2636
+ }
2637
+ });
2638
+ ```
2639
+
2640
+ ❌ **Don't:** Share state between scenarios
2641
+ ```typescript
2642
+ // Bad - scenarios depend on order
2643
+ ```
2644
+
2645
+ ✅ **Do:** Use Before/After hooks for isolation
2646
+ ```typescript
2647
+ // Good - each scenario is independent
2648
+ Before(async function () { /* initialize */ });
2649
+ After(async function () { /* cleanup */ });
2650
+ ```
2651
+
2652
+ ## bddgen Best Practices & Workflow
2653
+
2654
+ ### Effective bddgen Usage
2655
+
2656
+ 1. **Feature-First Development:**
2657
+ ```bash
2658
+ # 1. Write feature file
2659
+ cat > src/modules/users/features/user-creation.feature << 'EOF'
2660
+ Feature: User Creation
2661
+ Scenario: Create a new user
2662
+ Given a new user form is ready
2663
+ When the user submits valid data
2664
+ Then a new user should be created
2665
+ EOF
2666
+
2667
+ # 2. Run bddgen to auto-generate steps
2668
+ npm run generate:bdd
2669
+
2670
+ # 3. Implement generated step functions
2671
+ # Steps are now in src/modules/users/steps/userCreation.steps.ts
2672
+ ```
2673
+
2674
+ 2. **Automatic Step Generation Workflow:**
2675
+ ```bash
2676
+ # All these commands auto-run bddgen before execution
2677
+ npm run bdd # Generate + Run BDD tests
2678
+ npm run test # Generate + Run all tests
2679
+ npm run bdd:watch # Generate + Watch for changes
2680
+ npm test:debug # Generate + Debug mode
2681
+
2682
+ # Manual generation (if needed)
2683
+ npm run generate:bdd # Only generate without running tests
2684
+ ```
2685
+
2686
+ 3. **Always Run Tests with npm Scripts:**
2687
+ ```bash
2688
+ # ✅ Correct - Uses bddgen automatically
2689
+ npm run bdd
2690
+ npm test
2691
+ npm run bdd:watch
2692
+
2693
+ # ❌ Avoid - Skips bddgen
2694
+ npx cucumber-js
2695
+ npx playwright test
2696
+ ```
2697
+
2698
+ 4. **Keep Feature Files Synchronized:**
2699
+ - Feature file names should match step file names (e.g., `user-creation.feature` → `userCreation.steps.ts`)
2700
+ - When you rename a feature file, remove old step file and regenerate
2701
+ - Use consistent naming conventions across features and steps
2702
+
2703
+ 5. **Directory Structure for bddgen:**
2704
+ ```
2705
+ src/modules/
2706
+ ├── scheduling-group/
2707
+ │ ├── features/
2708
+ │ │ └── create-scheduling-group.feature ← Feature files here
2709
+ │ ├── steps/
2710
+ │ │ └── createSchedulingGroup.steps.ts ← Generated here
2711
+ │ ├── read-models/
2712
+ │ ├── queries/
2713
+ │ └── test-data/
2714
+ └── users/
2715
+ ├── features/
2716
+ │ └── user-management.feature
2717
+ ├── steps/
2718
+ │ └── userManagement.steps.ts
2719
+ └── ...
2720
+ ```
2721
+
2722
+ ### bddgen Configuration Best Practices
2723
+
2724
+ **Avoid Common Issues:**
2725
+
2726
+ ❌ **Don't:** Run tests without bddgen
2727
+ ```bash
2728
+ # Bad - steps not generated
2729
+ npx cucumber-js src/modules/**/features/*.feature
2730
+ ```
2731
+
2732
+ ✅ **Do:** Use npm scripts that include bddgen
2733
+ ```bash
2734
+ # Good - bddgen runs first
2735
+ npm run bdd
2736
+ ```
2737
+
2738
+ ❌ **Don't:** Manually create step files
2739
+ ```bash
2740
+ # Bad - might not match feature file structure
2741
+ # Manually writing src/modules/users/steps/userCreation.steps.ts
2742
+ ```
2743
+
2744
+ ✅ **Do:** Let bddgen generate step files
2745
+ ```bash
2746
+ # Good - accurate generation from feature files
2747
+ npm run generate:bdd
2748
+ ```
2749
+
2750
+ ❌ **Don't:** Put feature files in wrong directory
2751
+ ```bash
2752
+ # Bad - bddgen won't find them
2753
+ src/features/my-feature.feature
2754
+ ```
2755
+
2756
+ ✅ **Do:** Follow directory structure
2757
+ ```bash
2758
+ # Good - bddgen scans this location
2759
+ src/modules/scheduling-group/features/create-scheduling-group.feature
2760
+ ```
2761
+
2762
+ ### Troubleshooting bddgen Issues
2763
+
2764
+ **Issue: "bddgen command not found"**
2765
+ ```bash
2766
+ # Solution: Install it
2767
+ npm install --save-dev bddgen
2768
+ ```
2769
+
2770
+ **Issue: No step files generated**
2771
+ - Check feature files exist in `src/modules/**/*.feature`
2772
+ - Verify `bddgen.config.js` exists in project root
2773
+ - Ensure `steps` directory exists: `src/modules/**/steps`
2774
+ - Run: `npm run generate:bdd` with verbose output
2775
+
2776
+ **Issue: Generated steps don't match feature**
2777
+ - Check feature file has valid Gherkin syntax
2778
+ - Verify step text matches exactly (case-sensitive)
2779
+ - Remove old step files and regenerate: `npm run generate:bdd`
2780
+
2781
+ **Issue: TypeScript errors in generated steps**
2782
+ - Verify `tsconfig.json` has correct `paths` configuration
2783
+ - Ensure all imports use `@` aliases: `import { logger } from "@utils/logger"`
2784
+ - Check that all referenced modules exist
2785
+
2786
+ ### Test Execution Flow Diagram
2787
+
2788
+ ```
2789
+ User runs: npm run bdd
2790
+
2791
+ npm scripts/pretest runs
2792
+ ├─ npm run reset-db (clears DB)
2793
+ └─ npm run seed-db (populates test data)
2794
+
2795
+ npm run generate:bdd executes
2796
+ ├─ Scans src/modules/**/*.feature
2797
+ ├─ Generates src/modules/**/steps/*.steps.ts
2798
+ └─ Creates World interface + Before/After
2799
+
2800
+ cucumber-js launches
2801
+ ├─ Requires src/modules/**/*.steps.ts
2802
+ ├─ Parses .feature files
2803
+ ├─ Matches steps to definitions
2804
+ └─ Executes Before → Given → When → Then → After
2805
+
2806
+ Reports generated
2807
+ ├─ reports/cucumber-report.html (visual)
2808
+ └─ reports/cucumber-report.json (data)
2809
+ ```
2810
+
2811
+ ## Acceptance Criteria & Implementation Roadmap
2812
+
2813
+ ### POC Acceptance Criteria (Phase 0-2)
2814
+
2815
+ **Must Have:**
2816
+ - ✅ Seed runner creates deterministic test data with fixed IDs (Areas, Teams, Users)
2817
+ - ✅ Transaction-per-test isolation working (rollback on failure, commit on success)
2818
+ - ✅ Single DB-driven Create test (happy path) passes repeatedly 3+ times
2819
+ - ✅ Returned groupId matches database insert (invariant assertion)
2820
+ - ✅ AllocationMenu, Notes, Team mappings verified in DB post-create
2821
+ - ✅ SchedulingGroupHistory audit record created with correct operation + lastAmendedBy
2822
+ - ✅ Correlation ID fixture generates UUID and passes through logs
2823
+ - ✅ Branch with all Phase 0-1 code committed and CI green
2824
+
2825
+ **Nice to Have (Early Velocity):**
2826
+ - Area Admin vs System Admin permission test runs (parameterised)
2827
+ - Delete operation with cascade cleanup verified
2828
+ - One UI E2E smoke test (login → create → assert list)
2829
+
2830
+ ### Phase-by-Phase Checklist
2831
+
2832
+ **Phase 0 Complete When:**
2833
+ - [ ] Business rules document signed off (area auto-fill, allocationsMenu default, delete confirmation)
2834
+ - [ ] Test scope defined (CRUD + 3 user roles + edge cases)
2835
+ - [ ] Seam strategy agreed (DB stubs until API; mock-ready API abstraction)
2836
+ - [ ] Impersonation approach decided (header-based or DB role seed)
2837
+
2838
+ **Phase 1 Complete When:**
2839
+ - [ ] Seed dataset (.sql or ts seed runner) committed
2840
+ - [ ] Fixed IDs used throughout (Areas 10001+, Teams 20001+, Users 30001+)
2841
+ - [ ] Test container or local DB accepts x-test-user-id header (or DB role assignment works)
2842
+ - [ ] Dependency order documented (Area → Team → User → AreaPermission → SchedulingGroup)
2843
+
2844
+ **Phase 2 Complete When:**
2845
+ - [ ] API abstraction interface defined (ISchedulingGroupClient)
2846
+ - [ ] DB-stub implementation of all CRUD methods ready
2847
+ - [ ] API client supports x-correlation-id and x-test-user-id headers
2848
+ - [ ] Mock and real implementations can be swapped in config/tests
2849
+
2850
+ **Phase 3 Complete When:**
2851
+ - [ ] Seed runner invoked in pretest hook automatically
2852
+ - [ ] resetSchedulingGroupTables(), fullReset() helpers tested
2853
+ - [ ] Before/After hooks handle transaction or reseed
2854
+ - [ ] Cleanup order verified (history, teams, group) to avoid FK violations
2855
+
2856
+ **Phase 4 Complete When:**
2857
+ - [ ] All DB-driven smoke test scenarios pass (at least 10 scenarios)
2858
+ - [ ] Permission matrix parameterised tests run (18 scenarios × 4 actions)
2859
+ - [ ] UI E2E create + history view passes end-to-end
2860
+ - [ ] Negative tests (duplicate, missing FK, invalid area) all pass
2861
+ - [ ] Concurrency test passes 5+ times without flake
2862
+
2863
+ **Phase 5 Complete When:**
2864
+ - [ ] Correlation ID generated per test and logged consistently
2865
+ - [ ] Request payload, response, and DB query results in structured logs
2866
+ - [ ] Logs include timestamp, correlationId, level, message, context
2867
+ - [ ] CI artifact includes logs with correlation IDs for triage
2868
+
2869
+ **Phase 6 Complete When:**
2870
+ - [ ] CI job sequence: reset DB → seed → run tests → publish report
2871
+ - [ ] Gate: no merge unless all tests pass + report generated
2872
+ - [ ] Gating prevents deploy to prod (DB destructive ops require test env)
2873
+ - [ ] Team can run CI locally: `npm run reset-db && npm run seed-db && npm run bdd`
2874
+
2875
+ ### Next Recommended Single Action
2876
+
2877
+ **Implement the seed + transaction fixture and a single DB-driven Create test:**
2878
+
2879
+ 1. **Create `scripts/seed-db-scheduling-group.ts`** (extends existing seed)
2880
+ ```typescript
2881
+ // Insert fixed-ID deterministic test data
2882
+ // Areas (10001-10009), Teams (20001-20099), Users (30001-30099)
2883
+ // Pre-existing SchedulingGroups for list/edit/delete tests
2884
+ ```
2885
+
2886
+ 2. **Add Before/After hooks to `step/schedulingGroupCrud.steps.ts`**
2887
+ ```typescript
2888
+ Before(async function() {
2889
+ await dbCleanup.resetSchedulingGroupTables();
2890
+ // transaction starts or test isolation enabled
2891
+ });
2892
+
2893
+ After(async function() {
2894
+ // cleanup created groups
2895
+ // transaction commits/rollbacks
2896
+ });
2897
+ ```
2898
+
2899
+ 3. **Implement Phase 4A happy-path scenario:**
2900
+ ```gherkin
2901
+ Scenario: System Admin creates new Scheduling Group (happy path)
2902
+ When the user creates the Scheduling Group via DB stub
2903
+ Then database assertions verify groupId, name, teams, history
2904
+ ```
2905
+
2906
+ 4. **Run locally:** `npm run bdd -- --tags @critical` (repeat 3x, should pass all 3)
2907
+
2908
+ 5. **Commit:** All seed, fixture, and first test to a branch; CI should pass
2909
+
2910
+ **Estimated Effort:** 4-6 hours (seed setup, transaction wiring, one full scenario + assertions)
2911
+ **ROI:** Unblocks remaining 40+ scenarios; validates entire Phase 4 approach
2912
+
2913
+ ---
2914
+
2915
+ ## Framework Integration Summary
2916
+
2917
+ **bddgen** + **Cucumber** + **Playwright** + **Fixtures** + **6-Phase Strategy** = Enterprise-Grade BDD Test Suite
2918
+
2919
+ ### How Everything Connects
2920
+
2921
+ ```
2922
+ 1. Business Context (BBC)
2923
+
2924
+ 2. User Stories (NP035.01-05)
2925
+
2926
+ 3. Feature Files (Gherkin scenarios with @tags for phases)
2927
+
2928
+ 4. bddgen auto-generates Step Definitions + World interface
2929
+
2930
+ 5. Fixtures (contextProvider supplies users, correlation IDs, test data builders)
2931
+
2932
+ 6. Step Implementations (call mutations, read-models, API client)
2933
+
2934
+ 7. Database Client (parameterized queries, connection pooling, transaction handling)
2935
+
2936
+ 8. Mutation Helpers (create, update, delete with full audit + validation)
2937
+
2938
+ 9. Seed Runner (deterministic test data with fixed IDs)
2939
+
2940
+ 10. Before/After Hooks (cleanup, isolation, correlation context)
2941
+
2942
+ 11. Logger + Correlation (structured logs for triage)
2943
+
2944
+ 12. CI/CD (reset → seed → run → report)
2945
+
2946
+ 13. Acceptance Criteria Met ✅
2947
+ ```
2948
+
2949
+ ### Configuration Files Recap
2950
+
2951
+ - **`playwright.config.ts`**: Defines projects (ui, api, db, integrated); reporters; baseURL
2952
+ - **`tsconfig.json`**: Path aliases (@test-harness, @fixtures, @utils) for clean imports
2953
+ - **`bddgen.config.js`**: Scans .feature files, generates .steps.ts with proper structure
2954
+ - **`cucumber.js`**: Executes BDD, reports HTML/JSON, supports parallel + async-await
2955
+ - **`.env`**: Database and API credentials (test-env safe values)
2956
+ - **`package.json`**: Dependencies (mssql, axios, @playwright/test, @cucumber/cucumber, bddgen) + npm scripts
2957
+
2958
+ ### Key Dependencies When Tests Run
2959
+
2960
+ ```
2961
+ npm run bdd
2962
+ ├─ npm run generate:bdd (bddgen scans features → generates steps)
2963
+ ├─ npm run reset-db (clears SchedulingGroup* tables)
2964
+ ├─ npm run seed-db (inserts deterministic Areas, Teams, Users, SchedulingGroups)
2965
+ ├─ cucumber-js (requires .steps.ts, parses .feature files)
2966
+ │ ├─ Before hook: context fixture setup + transaction start
2967
+ │ ├─ Given/When/Then: call mutations/read-models with user context
2968
+ │ ├─ Then assertions: DB invariant checks + history validation
2969
+ │ └─ After hook: delete test artifacts + transaction rollback/commit
2970
+ └─ Reports generated: reports/cucumber-report.html, .json
2971
+ ```
2972
+
2973
+ ---
2974
+
2975
+ ## Summary
2976
+
2977
+ This framework provides a **complete, enterprise-ready testing solution** for the Scheduling Group feature:
2978
+
2979
+ ✅ **Scope & Business Context**: BBC + 5 user stories fully documented
2980
+ ✅ **6-Phase Testing Strategy**: Phase 0-6 with clear deliverables & checklist
2981
+ ✅ **Seed Automation**: Deterministic, repeatable test data with fixed IDs
2982
+ ✅ **Database Abstraction**: Stubs → API-ready; mutations with audit trails
2983
+ ✅ **Permission Validation**: Parameterised matrix covering all role/area combinations
2984
+ ✅ **Observability**: Correlation IDs, structured logging, fast triage
2985
+ ✅ **BDD Scenarios**: 40+ scenarios covering happy path, edge cases, concurrency, UI
2986
+ ✅ **CI/CD Ready**: Reproducible test runs, gated pipeline, artifact publishing
2987
+ ✅ **Immediate Action**: Single ROI-high seed + first test provides fast feedback
2988
+
2989
+ **Framework is ready to onboard team and execute Phase 0 kick-off.**