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.**
|