@solidxai/core 0.1.10-alpha.1 → 0.1.10-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dtos/update-user.dto.d.ts +1 -0
- package/dist/dtos/update-user.dto.d.ts.map +1 -1
- package/dist/dtos/update-user.dto.js +7 -1
- package/dist/dtos/update-user.dto.js.map +1 -1
- package/dist/entities/user.entity.js +1 -0
- package/dist/entities/user.entity.js.map +1 -1
- package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts.map +1 -1
- package/dist/jobs/database/chatter-queue-subscriber-database.service.js +3 -3
- package/dist/jobs/database/chatter-queue-subscriber-database.service.js.map +1 -1
- package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.d.ts.map +1 -1
- package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.js +3 -3
- package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.js.map +1 -1
- package/dist/jobs/redis/chatter-queue-subscriber-redis.service.d.ts.map +1 -1
- package/dist/jobs/redis/chatter-queue-subscriber-redis.service.js +3 -3
- package/dist/jobs/redis/chatter-queue-subscriber-redis.service.js.map +1 -1
- package/dist/seeders/module-test-data.service.d.ts +7 -0
- package/dist/seeders/module-test-data.service.d.ts.map +1 -1
- package/dist/seeders/module-test-data.service.js +94 -18
- package/dist/seeders/module-test-data.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +21 -0
- package/dist/services/chatter-message.service.d.ts +6 -3
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +23 -35
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/testing/reporter/console-reporter.d.ts +10 -0
- package/dist/testing/reporter/console-reporter.d.ts.map +1 -1
- package/dist/testing/reporter/console-reporter.js +21 -0
- package/dist/testing/reporter/console-reporter.js.map +1 -1
- package/dist/testing/reporter/reporter.types.d.ts +7 -0
- package/dist/testing/reporter/reporter.types.d.ts.map +1 -1
- package/dist/testing/reporter/reporter.types.js.map +1 -1
- package/dist/testing/runner/run-from-metadata.d.ts.map +1 -1
- package/dist/testing/runner/run-from-metadata.js +20 -1
- package/dist/testing/runner/run-from-metadata.js.map +1 -1
- package/package.json +1 -1
- package/src/dtos/update-user.dto.ts +4 -0
- package/src/entities/user.entity.ts +1 -1
- package/src/jobs/database/chatter-queue-subscriber-database.service.ts +4 -2
- package/src/jobs/rabbitmq/chatter-queue-subscriber.service.ts +4 -2
- package/src/jobs/redis/chatter-queue-subscriber-redis.service.ts +10 -3
- package/src/seeders/module-test-data.service.ts +107 -15
- package/src/seeders/seed-data/solid-core-metadata.json +21 -0
- package/src/services/chatter-message.service.ts +27 -37
- package/src/testing/reporter/console-reporter.ts +27 -0
- package/src/testing/reporter/reporter.types.ts +7 -0
- package/src/testing/runner/run-from-metadata.ts +19 -1
|
@@ -33,7 +33,7 @@ export class ChatterQueueSubscriberRabbitmq extends RabbitMqSubscriber<AuditQueu
|
|
|
33
33
|
|
|
34
34
|
switch (p.eventType) {
|
|
35
35
|
case 'insert':
|
|
36
|
-
await this.chatterMessageService.postAuditMessageOnInsert(p.after, p.modelName);
|
|
36
|
+
await this.chatterMessageService.postAuditMessageOnInsert(p.after, p.modelName, false, p.userId);
|
|
37
37
|
break;
|
|
38
38
|
case 'update':
|
|
39
39
|
await this.chatterMessageService.postAuditMessageOnUpdate(
|
|
@@ -41,10 +41,12 @@ export class ChatterQueueSubscriberRabbitmq extends RabbitMqSubscriber<AuditQueu
|
|
|
41
41
|
p.modelName,
|
|
42
42
|
p.before,
|
|
43
43
|
(p.updatedColumnNames ?? []).map(n => ({ propertyName: n })),
|
|
44
|
+
false,
|
|
45
|
+
p.userId,
|
|
44
46
|
);
|
|
45
47
|
break;
|
|
46
48
|
case 'delete':
|
|
47
|
-
await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before);
|
|
49
|
+
await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before, false, p.userId);
|
|
48
50
|
break;
|
|
49
51
|
}
|
|
50
52
|
}
|
|
@@ -33,13 +33,20 @@ export class ChatterQueueSubscriberRedis extends RedisSubscriber<any> {
|
|
|
33
33
|
|
|
34
34
|
switch (p.eventType) {
|
|
35
35
|
case 'insert':
|
|
36
|
-
await this.chatterMessageService.postAuditMessageOnInsert(p.after,
|
|
36
|
+
await this.chatterMessageService.postAuditMessageOnInsert(p.after, p.modelName, false, p.userId);
|
|
37
37
|
break;
|
|
38
38
|
case 'update':
|
|
39
|
-
await this.chatterMessageService.postAuditMessageOnUpdate(
|
|
39
|
+
await this.chatterMessageService.postAuditMessageOnUpdate(
|
|
40
|
+
p.after,
|
|
41
|
+
p.modelName,
|
|
42
|
+
p.before,
|
|
43
|
+
(p.updatedColumnNames || []).map(n => ({ propertyName: n })),
|
|
44
|
+
false,
|
|
45
|
+
p.userId,
|
|
46
|
+
);
|
|
40
47
|
break;
|
|
41
48
|
case 'delete':
|
|
42
|
-
await this.chatterMessageService.postAuditMessageOnDelete(p.
|
|
49
|
+
await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before, false, p.userId);
|
|
43
50
|
break;
|
|
44
51
|
}
|
|
45
52
|
}
|
|
@@ -24,6 +24,7 @@ import { TestingRoleSpec, TestingUserSpec } from 'src/testing/contracts/testing-
|
|
|
24
24
|
@Injectable()
|
|
25
25
|
export class ModuleTestDataService {
|
|
26
26
|
private readonly logger = new Logger(ModuleTestDataService.name);
|
|
27
|
+
private static readonly TEARDOWN_RETRY_ATTEMPTS = 5;
|
|
27
28
|
|
|
28
29
|
constructor(
|
|
29
30
|
private readonly moduleRef: ModuleRef,
|
|
@@ -665,31 +666,122 @@ export class ModuleTestDataService {
|
|
|
665
666
|
private async dropTestDatabaseObjects(databases: Record<string, string>): Promise<void> {
|
|
666
667
|
const entries = Object.entries(databases);
|
|
667
668
|
for (const [dsName, dbName] of entries) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
669
|
+
await this.dropTestDatabaseObjectsWithRetry(dsName, dbName);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
672
|
|
|
673
|
-
|
|
673
|
+
private async dropTestDatabaseObjectsWithRetry(dsName: string, dbName: string): Promise<void> {
|
|
674
|
+
let lastError: unknown;
|
|
675
|
+
|
|
676
|
+
for (let attempt = 1; attempt <= ModuleTestDataService.TEARDOWN_RETRY_ATTEMPTS; attempt += 1) {
|
|
677
|
+
console.log(`Attempting to tear down "${dbName}" on datasource "${dsName}" (${attempt}/${ModuleTestDataService.TEARDOWN_RETRY_ATTEMPTS})...`);
|
|
674
678
|
|
|
675
|
-
const queryRunner = dataSource.createQueryRunner();
|
|
676
679
|
try {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
await queryRunner.query(`DROP DATABASE IF EXISTS \`${dbName}\``);
|
|
684
|
-
} else {
|
|
685
|
-
throw new Error(`Unsupported database type for test data deletion: ${type}`);
|
|
680
|
+
await this.dropSingleTestDatabaseObject(dsName, dbName);
|
|
681
|
+
return;
|
|
682
|
+
} catch (error) {
|
|
683
|
+
lastError = error;
|
|
684
|
+
if (attempt >= ModuleTestDataService.TEARDOWN_RETRY_ATTEMPTS) {
|
|
685
|
+
throw error;
|
|
686
686
|
}
|
|
687
|
+
|
|
688
|
+
await this.sleep(this.teardownRetryDelayMs(attempt));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private teardownRetryDelayMs(attempt: number): number {
|
|
696
|
+
const baseMs = 500;
|
|
697
|
+
const incrementMs = 350;
|
|
698
|
+
const jitterMs = Math.floor(Math.random() * 250);
|
|
699
|
+
return baseMs + ((attempt - 1) * incrementMs) + jitterMs;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private async sleep(ms: number): Promise<void> {
|
|
703
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private async dropSingleTestDatabaseObject(dsName: string, dbName: string): Promise<void> {
|
|
707
|
+
const dataSource = this.resolveDataSourceByName(dsName);
|
|
708
|
+
|
|
709
|
+
if (dataSource.options.type === 'postgres') {
|
|
710
|
+
await this.dropPostgresDatabase(dataSource, dbName);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (!dataSource.isInitialized) {
|
|
715
|
+
await dataSource.initialize();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const queryRunner = dataSource.createQueryRunner();
|
|
719
|
+
try {
|
|
720
|
+
const type = dataSource.options.type;
|
|
721
|
+
if (type === 'mssql') {
|
|
722
|
+
await this.dropMssqlSchema(queryRunner, dbName);
|
|
723
|
+
} else if (type === 'mysql' || type === 'mariadb') {
|
|
724
|
+
await queryRunner.query(`DROP DATABASE IF EXISTS \`${dbName}\``);
|
|
725
|
+
} else {
|
|
726
|
+
throw new Error(`Unsupported database type for test data deletion: ${type}`);
|
|
727
|
+
}
|
|
728
|
+
} finally {
|
|
729
|
+
await queryRunner.release();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private async dropPostgresDatabase(dataSource: DataSource, dbName: string): Promise<void> {
|
|
734
|
+
if (dataSource.isInitialized) {
|
|
735
|
+
await dataSource.destroy();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const adminDataSource = new DataSource({
|
|
739
|
+
...(dataSource.options as any),
|
|
740
|
+
database: this.resolvePostgresMaintenanceDatabase(dataSource),
|
|
741
|
+
name: `${String(dataSource.name ?? 'default')}_teardown_admin_${Date.now()}`,
|
|
742
|
+
synchronize: false,
|
|
743
|
+
migrationsRun: false,
|
|
744
|
+
entities: [],
|
|
745
|
+
subscribers: [],
|
|
746
|
+
migrations: [],
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
await adminDataSource.initialize();
|
|
751
|
+
const queryRunner = adminDataSource.createQueryRunner();
|
|
752
|
+
try {
|
|
753
|
+
await queryRunner.query(
|
|
754
|
+
`SELECT pg_terminate_backend(pid)
|
|
755
|
+
FROM pg_stat_activity
|
|
756
|
+
WHERE datname = $1
|
|
757
|
+
AND pid <> pg_backend_pid()`,
|
|
758
|
+
[dbName],
|
|
759
|
+
);
|
|
760
|
+
await queryRunner.query(`DROP DATABASE IF EXISTS "${dbName}"`);
|
|
687
761
|
} finally {
|
|
688
762
|
await queryRunner.release();
|
|
689
763
|
}
|
|
764
|
+
} finally {
|
|
765
|
+
if (adminDataSource.isInitialized) {
|
|
766
|
+
await adminDataSource.destroy();
|
|
767
|
+
}
|
|
690
768
|
}
|
|
691
769
|
}
|
|
692
770
|
|
|
771
|
+
private resolvePostgresMaintenanceDatabase(dataSource: DataSource): string {
|
|
772
|
+
const configured = process.env.POSTGRES_MAINTENANCE_DATABASE?.trim();
|
|
773
|
+
if (configured) {
|
|
774
|
+
return configured;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const currentDb = String((dataSource.options as any)?.database ?? '').trim();
|
|
778
|
+
if (currentDb && currentDb !== 'postgres') {
|
|
779
|
+
return 'postgres';
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return 'template1';
|
|
783
|
+
}
|
|
784
|
+
|
|
693
785
|
private async dropMssqlSchema(queryRunner: ReturnType<DataSource['createQueryRunner']>, schemaName: string): Promise<void> {
|
|
694
786
|
const foreignKeys: Array<{ fk_name: string; table_name: string }> = await queryRunner.query(
|
|
695
787
|
`SELECT fk.name AS fk_name, t.name AS table_name
|
|
@@ -1761,6 +1761,19 @@
|
|
|
1761
1761
|
"encrypt": false,
|
|
1762
1762
|
"isSystem": true
|
|
1763
1763
|
},
|
|
1764
|
+
{
|
|
1765
|
+
"name": "failedLoginAttempts",
|
|
1766
|
+
"displayName": "Failed Login Attempts",
|
|
1767
|
+
"type": "int",
|
|
1768
|
+
"ormType": "integer",
|
|
1769
|
+
"defaultValue": "0",
|
|
1770
|
+
"required": false,
|
|
1771
|
+
"unique": false,
|
|
1772
|
+
"index": false,
|
|
1773
|
+
"private": false,
|
|
1774
|
+
"encrypt": false,
|
|
1775
|
+
"isSystem": true
|
|
1776
|
+
},
|
|
1764
1777
|
{
|
|
1765
1778
|
"name": "profilePicture",
|
|
1766
1779
|
"displayName": "Profile Picture",
|
|
@@ -9841,6 +9854,14 @@
|
|
|
9841
9854
|
"name": "active",
|
|
9842
9855
|
"isSearchable": true
|
|
9843
9856
|
}
|
|
9857
|
+
},
|
|
9858
|
+
{
|
|
9859
|
+
"type": "field",
|
|
9860
|
+
"attrs": {
|
|
9861
|
+
"label" : "Blocked / Unblocked",
|
|
9862
|
+
"name": "failedLoginAttempts",
|
|
9863
|
+
"viewWidget": "SolidUserBlockedStatusListWidget"
|
|
9864
|
+
}
|
|
9844
9865
|
}
|
|
9845
9866
|
]
|
|
9846
9867
|
}
|
|
@@ -47,6 +47,26 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
47
47
|
super(entityManager, repo, 'chatterMessage', 'solid-core', moduleRef);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
private resolveMessageUserId(userId?: number | null): number | null {
|
|
51
|
+
if (userId) {
|
|
52
|
+
return userId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return this.requestContextService.getActiveUser()?.sub ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private resolveMessageUser(userId?: number | null) {
|
|
59
|
+
const resolvedUserId = this.resolveMessageUserId(userId);
|
|
60
|
+
return resolvedUserId ? ({ id: resolvedUserId } as any) : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private stampMessageAuditFields(chatterMessage: ChatterMessage, userId?: number | null) {
|
|
64
|
+
const resolvedUserId = this.resolveMessageUserId(userId);
|
|
65
|
+
chatterMessage.user = resolvedUserId ? ({ id: resolvedUserId } as any) : null;
|
|
66
|
+
chatterMessage.createdBy = resolvedUserId;
|
|
67
|
+
chatterMessage.updatedBy = resolvedUserId;
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
async markCompleted(id: number) {
|
|
51
71
|
const activeUser = this.requestContextService.getActiveUser();
|
|
52
72
|
if (!activeUser) {
|
|
@@ -78,14 +98,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
78
98
|
});
|
|
79
99
|
chatterMessage.modelDisplayName = model?.displayName ?? null;
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (activeUser) {
|
|
84
|
-
const userId = activeUser?.sub;
|
|
85
|
-
chatterMessage.user = { id: userId } as any;
|
|
86
|
-
} else {
|
|
87
|
-
chatterMessage.user = null;
|
|
88
|
-
}
|
|
101
|
+
this.stampMessageAuditFields(chatterMessage);
|
|
89
102
|
|
|
90
103
|
const savedMessage = await this.repo.save(chatterMessage);
|
|
91
104
|
|
|
@@ -114,7 +127,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
114
127
|
return savedMessage;
|
|
115
128
|
}
|
|
116
129
|
|
|
117
|
-
async postAuditMessageOnInsert(entity: any, modelName: string, messageQueue: boolean = false) {
|
|
130
|
+
async postAuditMessageOnInsert(entity: any, modelName: string, messageQueue: boolean = false, userId?: number | null) {
|
|
118
131
|
if (!entity) {
|
|
119
132
|
return;
|
|
120
133
|
}
|
|
@@ -139,8 +152,6 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
139
152
|
!(field.type === 'relation' && field.relationType === 'one-to-many')
|
|
140
153
|
);
|
|
141
154
|
|
|
142
|
-
const activeUser = this.requestContextService.getActiveUser();
|
|
143
|
-
|
|
144
155
|
const chatterMessage = new ChatterMessage();
|
|
145
156
|
chatterMessage.messageType = CHATTER_MESSAGE_TYPE.AUDIT;
|
|
146
157
|
chatterMessage.messageSubType = CHATTER_MESSAGE_SUBTYPE.AUDIT_INSERT;
|
|
@@ -150,13 +161,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
150
161
|
chatterMessage.modelDisplayName = model?.displayName;
|
|
151
162
|
chatterMessage.modelUserKey = entity[model?.userKeyField?.name];
|
|
152
163
|
chatterMessage.messageBody = `New ${model?.displayName} created`;
|
|
153
|
-
|
|
154
|
-
if (activeUser) {
|
|
155
|
-
const userId = activeUser?.sub;
|
|
156
|
-
chatterMessage.user = { id: userId } as any;
|
|
157
|
-
} else {
|
|
158
|
-
chatterMessage.user = null;
|
|
159
|
-
}
|
|
164
|
+
this.stampMessageAuditFields(chatterMessage, userId);
|
|
160
165
|
|
|
161
166
|
const savedMessage = await this.repo.save(chatterMessage);
|
|
162
167
|
|
|
@@ -177,7 +182,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
184
|
|
|
180
|
-
async postAuditMessageOnUpdate(entity: any, modelName: string, databaseEntity: any, updatedColumns: any[] = [], messageQueue: boolean = false) {
|
|
185
|
+
async postAuditMessageOnUpdate(entity: any, modelName: string, databaseEntity: any, updatedColumns: any[] = [], messageQueue: boolean = false, userId?: number | null) {
|
|
181
186
|
if (!databaseEntity || !entity) {
|
|
182
187
|
return;
|
|
183
188
|
}
|
|
@@ -259,8 +264,6 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
259
264
|
return;
|
|
260
265
|
}
|
|
261
266
|
|
|
262
|
-
const activeUser = this.requestContextService.getActiveUser();
|
|
263
|
-
|
|
264
267
|
const chatterMessage = new ChatterMessage();
|
|
265
268
|
chatterMessage.messageType = CHATTER_MESSAGE_TYPE.AUDIT;
|
|
266
269
|
chatterMessage.messageSubType = CHATTER_MESSAGE_SUBTYPE.AUDIT_UPDATE;
|
|
@@ -270,13 +273,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
270
273
|
chatterMessage.modelDisplayName = model.displayName;
|
|
271
274
|
chatterMessage.modelUserKey = entity[model?.userKeyField?.name];
|
|
272
275
|
chatterMessage.messageBody = `${model?.displayName} updated`;
|
|
273
|
-
|
|
274
|
-
if (activeUser) {
|
|
275
|
-
const userId = activeUser?.sub;
|
|
276
|
-
chatterMessage.user = { id: userId } as any;
|
|
277
|
-
} else {
|
|
278
|
-
chatterMessage.user = null;
|
|
279
|
-
}
|
|
276
|
+
this.stampMessageAuditFields(chatterMessage, userId);
|
|
280
277
|
|
|
281
278
|
const savedMessage = await this.repo.save(chatterMessage);
|
|
282
279
|
|
|
@@ -294,7 +291,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
294
291
|
}
|
|
295
292
|
}
|
|
296
293
|
|
|
297
|
-
async postAuditMessageOnDelete(modelName: string, databaseEntity: any, messageQueue: boolean = false) {
|
|
294
|
+
async postAuditMessageOnDelete(modelName: string, databaseEntity: any, messageQueue: boolean = false, userId?: number | null) {
|
|
298
295
|
const model = await this.modelMetadataRepo.findOne({
|
|
299
296
|
where: {
|
|
300
297
|
singularName: lowerFirst(modelName)
|
|
@@ -335,14 +332,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
335
332
|
chatterMessage.modelUserKey = databaseEntity[model?.userKeyField?.name];
|
|
336
333
|
chatterMessage.messageBody = `${model?.displayName} deleted`;
|
|
337
334
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (activeUser) {
|
|
341
|
-
const userId = activeUser?.sub;
|
|
342
|
-
chatterMessage.user = { id: userId } as any;
|
|
343
|
-
} else {
|
|
344
|
-
chatterMessage.user = null;
|
|
345
|
-
}
|
|
335
|
+
this.stampMessageAuditFields(chatterMessage, userId);
|
|
346
336
|
|
|
347
337
|
const savedMessage = await this.repo.save(chatterMessage);
|
|
348
338
|
|
|
@@ -146,6 +146,10 @@ function formatStepLabel(step: OpStep): string {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
export class ConsoleReporter implements Reporter {
|
|
149
|
+
private totalScenarios = 0;
|
|
150
|
+
private passedScenarios = 0;
|
|
151
|
+
private failedScenarios = 0;
|
|
152
|
+
|
|
149
153
|
onScenarioStart(scenario: { id: string; name?: string }): void {
|
|
150
154
|
const label = scenario.name ? `${scenario.id} (${scenario.name})` : scenario.id;
|
|
151
155
|
console.log(`\n▶ Scenario: ${label}`);
|
|
@@ -157,6 +161,12 @@ export class ConsoleReporter implements Reporter {
|
|
|
157
161
|
): void {
|
|
158
162
|
const label = scenario.name ? `${scenario.id} (${scenario.name})` : scenario.id;
|
|
159
163
|
const status = result.ok ? "✔" : "✖";
|
|
164
|
+
this.totalScenarios += 1;
|
|
165
|
+
if (result.ok) {
|
|
166
|
+
this.passedScenarios += 1;
|
|
167
|
+
} else {
|
|
168
|
+
this.failedScenarios += 1;
|
|
169
|
+
}
|
|
160
170
|
console.log(`${status} Scenario: ${label} (${result.durationMs}ms)`);
|
|
161
171
|
}
|
|
162
172
|
|
|
@@ -226,4 +236,21 @@ export class ConsoleReporter implements Reporter {
|
|
|
226
236
|
if (!dataText.length) return;
|
|
227
237
|
console.log(indentLines(dataText, `${STEP_INDENT}${INDENT}${INDENT}`));
|
|
228
238
|
}
|
|
239
|
+
|
|
240
|
+
onRunEnd(args: {
|
|
241
|
+
ok: boolean;
|
|
242
|
+
total: number;
|
|
243
|
+
passed: number;
|
|
244
|
+
failed: number;
|
|
245
|
+
durationMs: number;
|
|
246
|
+
}): void {
|
|
247
|
+
const durationSeconds = (args.durationMs / 1000).toFixed(2);
|
|
248
|
+
const finalStatus = args.ok ? "PASSED" : "FAILED";
|
|
249
|
+
|
|
250
|
+
console.log("\n════════ Test Run Summary ════════");
|
|
251
|
+
console.log(`Result: Test run ${finalStatus}`);
|
|
252
|
+
console.log(`Cases: total=${args.total}, passed=${args.passed}, failed=${args.failed}`);
|
|
253
|
+
console.log(`Duration: ${durationSeconds}s`);
|
|
254
|
+
console.log("══════════════════════════════════");
|
|
255
|
+
}
|
|
229
256
|
}
|
|
@@ -44,6 +44,7 @@ export type RunnerOptions = {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
export async function runFromMetadata(opts: RunnerOptions): Promise<void> {
|
|
47
|
+
const startedAt = Date.now();
|
|
47
48
|
const registry = new StepRegistry();
|
|
48
49
|
registerApiSteps(registry);
|
|
49
50
|
registerUiSteps(registry);
|
|
@@ -71,15 +72,32 @@ export async function runFromMetadata(opts: RunnerOptions): Promise<void> {
|
|
|
71
72
|
const ui = new PlaywrightAdapter(opts.ui);
|
|
72
73
|
const ctxBase = { resources, reporter, api, ui, specRegistry, testData, options: opts.options };
|
|
73
74
|
const uiStarted = { value: false };
|
|
75
|
+
let passed = 0;
|
|
76
|
+
let failed = 0;
|
|
77
|
+
let runError: unknown;
|
|
74
78
|
|
|
75
79
|
try {
|
|
76
80
|
for (const scenario of scenarios) {
|
|
77
81
|
if (scenarioNeedsUi(scenario)) {
|
|
78
82
|
await ensureUiStarted(ctxBase, uiStarted);
|
|
79
83
|
}
|
|
80
|
-
|
|
84
|
+
try {
|
|
85
|
+
await engine.runScenario(scenario, ctxBase);
|
|
86
|
+
passed += 1;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
failed += 1;
|
|
89
|
+
runError = error;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
81
92
|
}
|
|
82
93
|
} finally {
|
|
94
|
+
reporter.onRunEnd?.({
|
|
95
|
+
ok: !runError,
|
|
96
|
+
total: scenarios.length,
|
|
97
|
+
passed,
|
|
98
|
+
failed,
|
|
99
|
+
durationMs: Date.now() - startedAt,
|
|
100
|
+
});
|
|
83
101
|
if (uiStarted.value) {
|
|
84
102
|
await ui.stop();
|
|
85
103
|
}
|