@solidxai/core 0.1.13-beta.11 → 0.1.13-beta.13
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/.claude/settings.local.json +16 -0
- package/CHANGELOG.md +1128 -0
- package/CLAUDE.md +26 -0
- package/dist/commands/run-tests.command.d.ts +2 -0
- package/dist/commands/run-tests.command.d.ts.map +1 -1
- package/dist/commands/run-tests.command.js +20 -3
- package/dist/commands/run-tests.command.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +1 -1
- package/dist/services/chatter-message.service.d.ts +4 -1
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +4 -1
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/services/crud-helper.service.d.ts +1 -2
- package/dist/services/crud-helper.service.d.ts.map +1 -1
- package/dist/services/crud-helper.service.js +2 -16
- package/dist/services/crud-helper.service.js.map +1 -1
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +1 -1
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/draft-publish-helper.service.d.ts +1 -0
- package/dist/services/draft-publish-helper.service.d.ts.map +1 -1
- package/dist/services/draft-publish-helper.service.js +38 -4
- package/dist/services/draft-publish-helper.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +62 -6
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +34 -3
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/testing/reporter/console-reporter.d.ts +3 -0
- package/dist/testing/reporter/console-reporter.d.ts.map +1 -1
- package/dist/testing/reporter/console-reporter.js +2 -0
- package/dist/testing/reporter/console-reporter.js.map +1 -1
- package/dist/testing/reporter/progress-reporter.d.ts +64 -0
- package/dist/testing/reporter/progress-reporter.d.ts.map +1 -0
- package/dist/testing/reporter/progress-reporter.js +224 -0
- package/dist/testing/reporter/progress-reporter.js.map +1 -0
- package/dist/testing/reporter/reporter.types.d.ts +3 -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 +1 -0
- package/dist/testing/runner/run-from-metadata.js.map +1 -1
- package/dist/theme/theme-registry.d.ts +14 -0
- package/dist/theme/theme-registry.d.ts.map +1 -0
- package/dist/theme/theme-registry.js +147 -0
- package/dist/theme/theme-registry.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/run-tests.command.ts +17 -3
- package/src/seeders/seed-data/solid-core-metadata.json +1 -1
- package/src/services/1.js +6 -0
- package/src/services/chatter-message.service.ts +22 -1
- package/src/services/crud-helper.service.ts +19 -16
- package/src/services/crud.service.ts +2 -1
- package/src/services/draft-publish-helper.service.ts +56 -4
- package/src/services/settings/default-settings-provider.service.ts +34 -3
- package/src/testing/README.md +2 -1
- package/src/testing/reporter/console-reporter.ts +4 -0
- package/src/testing/reporter/progress-reporter.ts +251 -0
- package/src/testing/reporter/reporter.types.ts +3 -0
- package/src/testing/runner/run-from-metadata.ts +1 -0
- package/src/theme/theme-registry.ts +181 -0
- package/dist-tests/api/authenticate.spec.js +0 -119
- package/dist-tests/api/authenticate.spec.js.map +0 -1
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
- package/dist-tests/api/ping.spec.js +0 -21
- package/dist-tests/api/ping.spec.js.map +0 -1
- package/dist-tests/helpers/auth.js +0 -41
- package/dist-tests/helpers/auth.js.map +0 -1
- package/dist-tests/helpers/env.js +0 -11
- package/dist-tests/helpers/env.js.map +0 -1
- package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +0 -136
- package/docs/agent-builder/team-ready-note-agent-builder.md +0 -305
- package/docs/agent-hub-grooming.md +0 -301
- package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
- package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
- package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
- package/docs/datasource-introspection-ddl-analysis.md +0 -326
- package/docs/datasource-introspection-implementation-plan.md +0 -306
- package/docs/grouping-enhancements.md +0 -89
- package/docs/java-spring/README.md +0 -3
- package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
- package/docs/module-package-import-handoff.md +0 -691
- package/docs/seed-changes.md +0 -65
- package/docs/test-data-workflow.md +0 -200
- package/docs/type-declaration-import-issue.md +0 -24
- package/docs/workflow/business-automation-example-notes.md +0 -309
- package/docs/workflow/control-flow-node-addition-sop.md +0 -324
- package/docs/workflow/data-engineering-pipeline-example-notes.md +0 -330
- package/docs/workflow/foreach-example-notes.md +0 -187
- package/docs/workflow/hello-world-example-notes.md +0 -271
- package/docs/workflow/kestra-concepts-plugins-blueprints.md +0 -315
- package/docs/workflow/loop-until-example-notes.md +0 -198
- package/docs/workflow/microservices-and-apis-example-notes.md +0 -264
- package/docs/workflow/samples.md +0 -25
- package/docs/workflow/what-is-kestra.md +0 -79
- package/docs/workflow/workflow-core-module-handoff-summary.md +0 -191
- package/docs/workflow/workflow-module-brd.md +0 -185
- package/docs/workflow/workflow-module-domain-model.md +0 -252
- package/docs/workflow/workflow-module-metadata-dsl.md +0 -302
- package/docs/workflow/workflow-module-technical-spec.md +0 -293
- package/docs/workflow/workflow-node-type-contracts.md +0 -635
- package/docs/workflow/workflow-node-ui-metadata-contract.md +0 -1262
- package/docs/workflow/workflow-solidx-model-field-spec.md +0 -284
|
@@ -3,6 +3,7 @@ import { SubCommand, CommandRunner, Option } from 'nest-commander';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service';
|
|
5
5
|
import { ConsoleReporter } from 'src/testing/reporter/console-reporter';
|
|
6
|
+
import { ProgressReporter } from 'src/testing/reporter/progress-reporter';
|
|
6
7
|
import { WebhookReporter } from 'src/testing/reporter/webhook-reporter';
|
|
7
8
|
import { runFromMetadata } from 'src/testing/runner/run-from-metadata';
|
|
8
9
|
import type { TestingMetadata } from 'src/testing/contracts/testing-metadata.types';
|
|
@@ -27,6 +28,7 @@ interface TestRunCommandOptions {
|
|
|
27
28
|
listSpecs?: boolean;
|
|
28
29
|
printApiLogs?: boolean;
|
|
29
30
|
runName?: string;
|
|
31
|
+
progress?: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
@SubCommand({
|
|
@@ -106,9 +108,12 @@ export class TestRunCommand extends CommandRunner {
|
|
|
106
108
|
|
|
107
109
|
const webhookUrl = process.env.SOLIDCTL_WEBHOOK_URL;
|
|
108
110
|
const runName = options?.runName ?? `run-${Date.now()}`;
|
|
109
|
-
const
|
|
111
|
+
const baseReporter = webhookUrl
|
|
110
112
|
? new WebhookReporter(webhookUrl, runName)
|
|
111
113
|
: new ConsoleReporter();
|
|
114
|
+
const reporter = options?.progress === false
|
|
115
|
+
? baseReporter
|
|
116
|
+
: new ProgressReporter(baseReporter);
|
|
112
117
|
|
|
113
118
|
try {
|
|
114
119
|
await runFromMetadata({
|
|
@@ -138,8 +143,8 @@ export class TestRunCommand extends CommandRunner {
|
|
|
138
143
|
console.error('Run tests command failed');
|
|
139
144
|
process.exitCode = 1;
|
|
140
145
|
} finally {
|
|
141
|
-
if (
|
|
142
|
-
await
|
|
146
|
+
if (baseReporter instanceof WebhookReporter) {
|
|
147
|
+
await baseReporter.flush(Number(process.exitCode ?? 0));
|
|
143
148
|
}
|
|
144
149
|
}
|
|
145
150
|
return;
|
|
@@ -289,6 +294,15 @@ export class TestRunCommand extends CommandRunner {
|
|
|
289
294
|
parseRunName(val: string): string {
|
|
290
295
|
return val;
|
|
291
296
|
}
|
|
297
|
+
|
|
298
|
+
@Option({
|
|
299
|
+
flags: '--no-progress',
|
|
300
|
+
description: 'Disable the interactive test progress footer.',
|
|
301
|
+
required: false,
|
|
302
|
+
})
|
|
303
|
+
parseNoProgress(): boolean {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
292
306
|
}
|
|
293
307
|
|
|
294
308
|
function splitCsv(value?: string): string[] | undefined {
|
|
@@ -12591,7 +12591,7 @@
|
|
|
12591
12591
|
50
|
|
12592
12592
|
],
|
|
12593
12593
|
"enableGlobalSearch": true,
|
|
12594
|
-
"create":
|
|
12594
|
+
"create": false,
|
|
12595
12595
|
"edit": true,
|
|
12596
12596
|
"delete": true,
|
|
12597
12597
|
"import": true,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
1. Do i need to create a storeStreams method for aws service too?
|
|
2
|
+
- Handle later
|
|
3
|
+
2. queues handling -> if queues is enabled by default, i.e triggerExport(exportTransactionEntity.id).
|
|
4
|
+
- startExport should either return the data or return the transaction id
|
|
5
|
+
3. How to handle scenarios wherein, nested related exist.(do i need to only get the userkey)
|
|
6
|
+
- show the userKey
|
|
@@ -37,6 +37,16 @@ interface ChatterMention {
|
|
|
37
37
|
id?: string | number;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Server-side-only overrides for `postMessage`. Not part of `PostChatterMessageDto`, so
|
|
42
|
+
* nothing here is settable over HTTP - only in-process callers such as migration or
|
|
43
|
+
* back-fill services can supply it.
|
|
44
|
+
*/
|
|
45
|
+
export interface PostChatterMessageOptions {
|
|
46
|
+
/** Backdate the message instead of stamping it with the current time. */
|
|
47
|
+
createdAt?: Date;
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
@Injectable()
|
|
41
51
|
export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
42
52
|
private readonly _logger = new Logger(ChatterMessageService.name);
|
|
@@ -389,7 +399,14 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
389
399
|
return this.trimMessageUser(savedMessage);
|
|
390
400
|
}
|
|
391
401
|
|
|
392
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Post a chatter message.
|
|
404
|
+
*
|
|
405
|
+
* `options` is deliberately kept out of `PostChatterMessageDto` so it can only be supplied
|
|
406
|
+
* by server-side callers (migrations, back-fills). The controller binds the DTO alone, so
|
|
407
|
+
* an API client has no way to backdate a message.
|
|
408
|
+
*/
|
|
409
|
+
async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = [], options: PostChatterMessageOptions = {}) {
|
|
393
410
|
const coModelName = lowerFirst(postDto.coModelName);
|
|
394
411
|
await this.assertRecordAccess(coModelName, postDto.coModelEntityId);
|
|
395
412
|
|
|
@@ -404,6 +421,10 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
404
421
|
chatterMessage.coModelEntityId = postDto.coModelEntityId;
|
|
405
422
|
chatterMessage.coModelName = coModelName;
|
|
406
423
|
chatterMessage.modelUserKey = postDto.modelUserKey ?? null;
|
|
424
|
+
// Left unset otherwise, so the @CreateDateColumn default still applies.
|
|
425
|
+
if (options.createdAt) {
|
|
426
|
+
chatterMessage.createdAt = options.createdAt;
|
|
427
|
+
}
|
|
407
428
|
|
|
408
429
|
const model = await this.modelMetadataRepo.findOne({
|
|
409
430
|
where: { singularName: coModelName },
|
|
@@ -889,25 +889,25 @@ export class CrudHelperService {
|
|
|
889
889
|
};
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
-
async countGroupedRecords(qb: SelectQueryBuilder<any>, basicFilterDto: BasicFilterDto, entityAlias: string) { //TODO : Check how to pass a type to SelectQueryBuilder instead of any
|
|
893
|
-
|
|
894
|
-
|
|
892
|
+
// async countGroupedRecords(qb: SelectQueryBuilder<any>, basicFilterDto: BasicFilterDto, entityAlias: string, internationalisation?: boolean, draftPublishWorkflow?: boolean, moduleRef?: any) { //TODO : Check how to pass a type to SelectQueryBuilder instead of any
|
|
893
|
+
// const { limit, offset, ...rest } = basicFilterDto;
|
|
894
|
+
// const filteredDto = { ...rest, limit: undefined, offset: undefined };
|
|
895
895
|
|
|
896
|
-
|
|
896
|
+
// const filteredQB = this.buildFilterQuery(qb, filteredDto as BasicFilterDto, entityAlias, internationalisation, draftPublishWorkflow, moduleRef, FilterCombinator.AND, false, false);
|
|
897
897
|
|
|
898
|
-
|
|
898
|
+
// const groupByFields = this.normalize(filteredDto.groupBy);
|
|
899
899
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
900
|
+
// if (!groupByFields || groupByFields.length === 0) {
|
|
901
|
+
// throw new Error(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
|
|
902
|
+
// }
|
|
903
903
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
904
|
+
// this.applyGroupBySelections(filteredQB, groupByFields, entityAlias);
|
|
905
|
+
// this.applyAggregates(filteredQB, ['count'], entityAlias);
|
|
906
|
+
// filteredQB.limit(undefined).offset(undefined).take(undefined).skip(undefined);
|
|
907
907
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
}
|
|
908
|
+
// const rawResults = await filteredQB.getRawMany();
|
|
909
|
+
// return rawResults.length;
|
|
910
|
+
// }
|
|
911
911
|
|
|
912
912
|
hasReadPermissionOnModel = (activeUser: ActiveUserData, modelName: string) => {
|
|
913
913
|
const permissionNames = [`${classify(modelName)}Controller.findOne`, `${classify(modelName)}Controller.findMany`];
|
|
@@ -980,7 +980,10 @@ export class CrudHelperService {
|
|
|
980
980
|
basicFilterDto: BasicFilterDto,
|
|
981
981
|
alias: string,
|
|
982
982
|
createQbFn: () => Promise<SelectQueryBuilder<T>>,
|
|
983
|
-
postProcessEntities?: (entities: T[]) => Promise<void
|
|
983
|
+
postProcessEntities?: (entities: T[]) => Promise<void>,
|
|
984
|
+
internationalisation?: boolean,
|
|
985
|
+
draftPublishWorkflow?: boolean,
|
|
986
|
+
moduleRef?: any
|
|
984
987
|
): Promise<{ meta: { totalRecords: number }; groupMeta: any[]; groupRecords: any[] }> {
|
|
985
988
|
const groupByFields = this.normalize(basicFilterDto.groupBy);
|
|
986
989
|
if (!groupByFields.length) throw new BadRequestException(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
|
|
@@ -1015,7 +1018,7 @@ export class CrudHelperService {
|
|
|
1015
1018
|
aggregates: undefined,
|
|
1016
1019
|
sort: basicFilterDto.groupFilter?.sort,
|
|
1017
1020
|
};
|
|
1018
|
-
groupQb = this.buildFilterQuery(groupQb, groupFilterDto, alias);
|
|
1021
|
+
groupQb = this.buildFilterQuery(groupQb, groupFilterDto, alias, internationalisation, draftPublishWorkflow, moduleRef);
|
|
1019
1022
|
groupQb = this.buildGroupByRecordsQuery(groupQb, group, alias, groupAliasMap, aggregateAliasMap, groupExpressionMap);
|
|
1020
1023
|
const [entities, count] = await groupQb.getManyAndCount();
|
|
1021
1024
|
if (postProcessEntities) await postProcessEntities(entities);
|
|
@@ -583,7 +583,8 @@ private async prepareManyToManyAuditSnapshot(entity: T,id: number,modelSingularN
|
|
|
583
583
|
async (entities) => {
|
|
584
584
|
if (populateUserIdFields?.length) await this.handlePopulateUserIdFields(populateUserIdFields, entities);
|
|
585
585
|
if (populateMedia?.length) await this.handlePopulateMedia(populateMedia, entities);
|
|
586
|
-
}
|
|
586
|
+
},
|
|
587
|
+
internationalisation, draftPublishWorkflow, this.moduleRef
|
|
587
588
|
);
|
|
588
589
|
}
|
|
589
590
|
else {
|
|
@@ -333,13 +333,65 @@ export class DraftPublishHelperService {
|
|
|
333
333
|
throw new BadRequestException(`${modelName} with id ${id} is already unpublished`);
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
const chainId = entity.initialEntityVersionId || entity.id;
|
|
337
|
+
|
|
338
|
+
return repo.manager.transaction(async (manager) => {
|
|
339
|
+
const transactionalRepo = manager.getRepository(repo.metadata.target);
|
|
340
|
+
|
|
341
|
+
// Release the "na" published tracker before anything else can claim it, mirroring
|
|
342
|
+
// publishRecord's archive-others-then-publish ordering in reverse. unpublishRecord
|
|
343
|
+
// only ever acts on the latest version (see loadLatestEntityForPublishAction), so
|
|
344
|
+
// this always becomes a Draft rather than an Archived version — clear publishedAt
|
|
345
|
+
// to match applyCreateDefaults' Draft convention instead of leaving a stale date.
|
|
346
|
+
const unpublishedEntity = await transactionalRepo.save({
|
|
347
|
+
...entity,
|
|
348
|
+
isPublished: false,
|
|
349
|
+
publishedAt: null,
|
|
350
|
+
publishedTracker: this.createPublishedVersionTracker(entity.id),
|
|
351
|
+
updatedBy: activeUser?.sub ?? entity.updatedBy,
|
|
352
|
+
} as any) as unknown as T;
|
|
353
|
+
|
|
354
|
+
const chainVersions = await transactionalRepo.find({
|
|
355
|
+
where: [
|
|
356
|
+
{ initialEntityVersionId: chainId },
|
|
357
|
+
{ id: chainId },
|
|
358
|
+
] as any,
|
|
359
|
+
}) as unknown as T[];
|
|
360
|
+
|
|
361
|
+
const versionToRestore = this.pickMostRecentlyPublishedVersion(
|
|
362
|
+
chainVersions.filter(version => version.id !== entity.id),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
if (versionToRestore) {
|
|
366
|
+
await transactionalRepo.save({
|
|
367
|
+
...versionToRestore,
|
|
368
|
+
isPublished: true,
|
|
369
|
+
publishedAt: new Date(),
|
|
370
|
+
publishedTracker: "na",
|
|
371
|
+
} as any);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return unpublishedEntity;
|
|
340
375
|
});
|
|
341
376
|
}
|
|
342
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Among the other versions in a chain, find the one that was live most recently before
|
|
380
|
+
* being superseded (i.e. the "Archived" version unpublishRecord should restore). Ties on
|
|
381
|
+
* publishedAt fall back to the highest id, matching pickCurrentLocaleRecord's ordering.
|
|
382
|
+
*/
|
|
383
|
+
private pickMostRecentlyPublishedVersion<T extends CommonEntity>(versions: T[]): T | null {
|
|
384
|
+
const previouslyPublished = versions.filter(version => Boolean(version.publishedAt));
|
|
385
|
+
if (previouslyPublished.length === 0) return null;
|
|
386
|
+
|
|
387
|
+
return previouslyPublished.sort((a, b) => {
|
|
388
|
+
const aPublishedAt = new Date(a.publishedAt as any).getTime();
|
|
389
|
+
const bPublishedAt = new Date(b.publishedAt as any).getTime();
|
|
390
|
+
if (aPublishedAt !== bPublishedAt) return bPublishedAt - aPublishedAt;
|
|
391
|
+
return Number(b.id) - Number(a.id);
|
|
392
|
+
})[0];
|
|
393
|
+
}
|
|
394
|
+
|
|
343
395
|
/**
|
|
344
396
|
* Load the latest version of a draft/publish-enabled record for a publish/unpublish
|
|
345
397
|
* action, applying the two checks both actions share (workflow enabled, version is latest).
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
SettingDefinition,
|
|
7
7
|
SettingLevel,
|
|
8
8
|
} from "src/interfaces";
|
|
9
|
+
import { getDefaultThemeKey, getThemesByMode } from "src/theme/theme-registry";
|
|
9
10
|
|
|
10
11
|
export const DEFAULT_MEDIA_UPLOAD_DIR = "media-uploads";
|
|
11
12
|
export const DEFAULT_MEDIA_FILE_STORAGE_DIR = "media-files-storage";
|
|
@@ -224,14 +225,44 @@ const getSolidCoreSettings = (isProd: boolean) =>
|
|
|
224
225
|
},
|
|
225
226
|
{
|
|
226
227
|
moduleName: "solid-core",
|
|
227
|
-
key: "
|
|
228
|
+
key: "showThemeToggle",
|
|
228
229
|
value: true,
|
|
229
230
|
level: SettingLevel.SystemAdminEditable,
|
|
230
|
-
label: "
|
|
231
|
+
label: "Show Theme Toggle",
|
|
231
232
|
group: "app-settings",
|
|
232
233
|
sortOrder: 120,
|
|
233
234
|
controlType: "boolean",
|
|
234
|
-
helpText: "
|
|
235
|
+
helpText: "Shows the theme toggle in the frontend so users can switch between light and dark themes.",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
moduleName: "solid-core",
|
|
239
|
+
key: "lightTheme",
|
|
240
|
+
value: getDefaultThemeKey("light"),
|
|
241
|
+
level: SettingLevel.SystemAdminEditable,
|
|
242
|
+
label: "Light Theme",
|
|
243
|
+
group: "app-settings",
|
|
244
|
+
sortOrder: 125,
|
|
245
|
+
controlType: "selectionStatic",
|
|
246
|
+
options: getThemesByMode("light").map((theme) => ({
|
|
247
|
+
label: theme.label,
|
|
248
|
+
value: theme.key,
|
|
249
|
+
})),
|
|
250
|
+
helpText: "Selects the theme used whenever the application is in light mode.",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
moduleName: "solid-core",
|
|
254
|
+
key: "darkTheme",
|
|
255
|
+
value: getDefaultThemeKey("dark"),
|
|
256
|
+
level: SettingLevel.SystemAdminEditable,
|
|
257
|
+
label: "Dark Theme",
|
|
258
|
+
group: "app-settings",
|
|
259
|
+
sortOrder: 126,
|
|
260
|
+
controlType: "selectionStatic",
|
|
261
|
+
options: getThemesByMode("dark").map((theme) => ({
|
|
262
|
+
label: theme.label,
|
|
263
|
+
value: theme.key,
|
|
264
|
+
})),
|
|
265
|
+
helpText: "Selects the theme used whenever the application is in dark mode.",
|
|
235
266
|
},
|
|
236
267
|
{
|
|
237
268
|
moduleName: "solid-core",
|
package/src/testing/README.md
CHANGED
|
@@ -479,10 +479,11 @@ Common `test run` flags (all pass through solidctl unchanged):
|
|
|
479
479
|
- `--ui-navigation-timeout-ms <number>` — default wait for `ui.goto`
|
|
480
480
|
- `--timeout-ms <number>` / `--retries <number>` — scenario-level guard and retry count
|
|
481
481
|
- `--scenario-ids <ids>` / `--include-tags <tags>` / `--skip-scenario-ids <ids>`
|
|
482
|
+
- `--no-progress` — disable the interactive test progress footer
|
|
482
483
|
|
|
483
484
|
## Add A New Step (SOP)
|
|
484
485
|
1. Create a new `*.step.ts` in the right domain folder.
|
|
485
486
|
2. Implement a `registerXSteps(registry)` function and `registry.register("op.name", handler)`.
|
|
486
487
|
3. Validate required `step.with` fields and throw clear errors.
|
|
487
488
|
4. Use adapters via `ctx.api` / `ctx.ui` and update `ctx.last` when helpful.
|
|
488
|
-
5. Export and register it from the domain `index.ts`.
|
|
489
|
+
5. Export and register it from the domain `index.ts`.
|
|
@@ -150,6 +150,10 @@ export class ConsoleReporter implements Reporter {
|
|
|
150
150
|
private passedScenarios = 0;
|
|
151
151
|
private failedScenarios = 0;
|
|
152
152
|
|
|
153
|
+
onRunStart(_args: { total: number }): void {
|
|
154
|
+
// Progress-aware reporters use this hook; the plain console reporter has no setup.
|
|
155
|
+
}
|
|
156
|
+
|
|
153
157
|
onScenarioStart(scenario: { id: string; name?: string }): void {
|
|
154
158
|
const label = scenario.name ? `${scenario.id} (${scenario.name})` : scenario.id;
|
|
155
159
|
console.log(`\n▶ Scenario: ${label}`);
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import * as readline from "readline";
|
|
2
|
+
import type { StepPhase } from "../contracts/runtime-context.types";
|
|
3
|
+
import type { OpStep, ScenarioSpec } from "../contracts/testing-metadata.types";
|
|
4
|
+
import type { SolidTestSpecResult } from "../contracts/test-spec.types";
|
|
5
|
+
import type { Reporter } from "./reporter.types";
|
|
6
|
+
|
|
7
|
+
const ANSI_BOLD = "\u001b[1m";
|
|
8
|
+
const ANSI_BOLD_OFF = "\u001b[22m";
|
|
9
|
+
const ANSI_DIM = "\u001b[2m";
|
|
10
|
+
const ANSI_GREEN = "\u001b[32m";
|
|
11
|
+
const ANSI_COLOR_OFF = "\u001b[39m";
|
|
12
|
+
const ANSI_RED = "\u001b[31m";
|
|
13
|
+
const ANSI_INVERSE = "\u001b[7m";
|
|
14
|
+
const ANSI_INVERSE_OFF = "\u001b[27m";
|
|
15
|
+
|
|
16
|
+
export class ProgressReporter implements Reporter {
|
|
17
|
+
private startedAt = Date.now();
|
|
18
|
+
private total = 0;
|
|
19
|
+
private completed = 0;
|
|
20
|
+
private passed = 0;
|
|
21
|
+
private failed = 0;
|
|
22
|
+
private timer?: NodeJS.Timeout;
|
|
23
|
+
private rendered = false;
|
|
24
|
+
|
|
25
|
+
constructor(private readonly delegate: Reporter) {}
|
|
26
|
+
|
|
27
|
+
onRunStart(args: { total: number }): void {
|
|
28
|
+
this.total = args.total;
|
|
29
|
+
this.startedAt = Date.now();
|
|
30
|
+
this.completed = 0;
|
|
31
|
+
this.passed = 0;
|
|
32
|
+
this.failed = 0;
|
|
33
|
+
this.delegate.onRunStart?.(args);
|
|
34
|
+
this.startTimer();
|
|
35
|
+
this.renderFooter();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onScenarioStart(scenario: ScenarioSpec): void {
|
|
39
|
+
this.withFooterPaused(() => this.delegate.onScenarioStart(scenario));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onScenarioEnd(
|
|
43
|
+
scenario: ScenarioSpec,
|
|
44
|
+
result: { ok: boolean; error?: unknown; durationMs: number },
|
|
45
|
+
): void {
|
|
46
|
+
this.completed += 1;
|
|
47
|
+
if (result.ok) {
|
|
48
|
+
this.passed += 1;
|
|
49
|
+
} else {
|
|
50
|
+
this.failed += 1;
|
|
51
|
+
}
|
|
52
|
+
this.withFooterPaused(() => this.delegate.onScenarioEnd(scenario, result));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onStepStart(args: {
|
|
56
|
+
scenarioId: string;
|
|
57
|
+
phase: StepPhase;
|
|
58
|
+
step: OpStep;
|
|
59
|
+
}): void {
|
|
60
|
+
this.withFooterPaused(() => this.delegate.onStepStart(args));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onStepEnd(args: {
|
|
64
|
+
scenarioId: string;
|
|
65
|
+
phase: StepPhase;
|
|
66
|
+
step: OpStep;
|
|
67
|
+
ok: boolean;
|
|
68
|
+
error?: unknown;
|
|
69
|
+
durationMs: number;
|
|
70
|
+
}): void {
|
|
71
|
+
this.withFooterPaused(() => this.delegate.onStepEnd(args));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onSpecResult(args: {
|
|
75
|
+
scenarioId: string;
|
|
76
|
+
specId: string;
|
|
77
|
+
stepName?: string;
|
|
78
|
+
result: SolidTestSpecResult;
|
|
79
|
+
}): void {
|
|
80
|
+
this.withFooterPaused(() => this.delegate.onSpecResult?.(args));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
attach(args: {
|
|
84
|
+
scenarioId: string;
|
|
85
|
+
name: string;
|
|
86
|
+
contentType: string;
|
|
87
|
+
data: Buffer | string;
|
|
88
|
+
}): void {
|
|
89
|
+
this.withFooterPaused(() => this.delegate.attach?.(args));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onRunEnd(args: {
|
|
93
|
+
ok: boolean;
|
|
94
|
+
total: number;
|
|
95
|
+
passed: number;
|
|
96
|
+
failed: number;
|
|
97
|
+
durationMs: number;
|
|
98
|
+
}): void {
|
|
99
|
+
this.completed = args.passed + args.failed;
|
|
100
|
+
this.passed = args.passed;
|
|
101
|
+
this.failed = args.failed;
|
|
102
|
+
this.total = args.total;
|
|
103
|
+
this.stopTimer();
|
|
104
|
+
this.withFooterPaused(() => this.delegate.onRunEnd?.(args), false);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private startTimer(): void {
|
|
108
|
+
this.stopTimer();
|
|
109
|
+
if (!this.isInteractive()) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.timer = setInterval(() => this.renderFooter(), 1_000);
|
|
113
|
+
this.timer.unref?.();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private stopTimer(): void {
|
|
117
|
+
if (!this.timer) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
clearInterval(this.timer);
|
|
121
|
+
this.timer = undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private withFooterPaused(callback: () => void, renderAfter = true): void {
|
|
125
|
+
this.clearFooter();
|
|
126
|
+
callback();
|
|
127
|
+
if (renderAfter) {
|
|
128
|
+
this.renderFooter();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private renderFooter(): void {
|
|
133
|
+
if (!this.isInteractive()) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
138
|
+
const elapsed = formatElapsed(Date.now() - this.startedAt);
|
|
139
|
+
const progressText = `${this.completed}/${this.total}`;
|
|
140
|
+
const resultText = `${ANSI_GREEN}${this.passed} passed${ANSI_COLOR_OFF}${ANSI_DIM} | ${ANSI_BOLD_OFF}${this.failed ? ANSI_RED : ANSI_DIM}${this.failed} failed${this.failed ? ANSI_COLOR_OFF : ANSI_BOLD_OFF}`;
|
|
141
|
+
const footer = [
|
|
142
|
+
`${ANSI_BOLD}Tests${ANSI_BOLD_OFF}: ${this.renderProgressBar(terminalWidth)} ${progressText}`,
|
|
143
|
+
`${ANSI_BOLD}Elapsed${ANSI_BOLD_OFF}: ${elapsed}`,
|
|
144
|
+
resultText,
|
|
145
|
+
].join(`${ANSI_DIM} | ${ANSI_BOLD_OFF}`);
|
|
146
|
+
|
|
147
|
+
readline.clearLine(process.stdout, 0);
|
|
148
|
+
readline.cursorTo(process.stdout, 0);
|
|
149
|
+
process.stdout.write(truncateAnsiText(`${ANSI_INVERSE} ${footer} ${ANSI_INVERSE_OFF}`, terminalWidth));
|
|
150
|
+
this.rendered = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private clearFooter(): void {
|
|
154
|
+
if (!this.rendered || !this.isInteractive()) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
readline.clearLine(process.stdout, 0);
|
|
158
|
+
readline.cursorTo(process.stdout, 0);
|
|
159
|
+
this.rendered = false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private renderProgressBar(terminalWidth: number): string {
|
|
163
|
+
const width = terminalWidth >= 120 ? 30 : terminalWidth >= 90 ? 22 : 14;
|
|
164
|
+
const ratio = this.total > 0 ? Math.min(1, this.completed / this.total) : 0;
|
|
165
|
+
const filled = Math.round(width * ratio);
|
|
166
|
+
const empty = Math.max(0, width - filled);
|
|
167
|
+
return `[${ANSI_GREEN}${"=".repeat(filled)}${ANSI_COLOR_OFF}${" ".repeat(empty)}]`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private isInteractive(): boolean {
|
|
171
|
+
return Boolean(process.stdout.isTTY || process.env.SOLIDCTL_TEST_PROGRESS === "1");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatElapsed(durationMs: number): string {
|
|
176
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
177
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
178
|
+
const seconds = totalSeconds % 60;
|
|
179
|
+
if (minutes < 60) {
|
|
180
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
181
|
+
}
|
|
182
|
+
const hours = Math.floor(minutes / 60);
|
|
183
|
+
const remainingMinutes = minutes % 60;
|
|
184
|
+
return `${hours}:${remainingMinutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function truncateAnsiText(text: string, maxWidth: number): string {
|
|
188
|
+
if (maxWidth <= 0) {
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let visibleWidth = 0;
|
|
193
|
+
let index = 0;
|
|
194
|
+
let output = "";
|
|
195
|
+
let hasEscape = false;
|
|
196
|
+
|
|
197
|
+
while (index < text.length) {
|
|
198
|
+
if (text[index] === "\u001b") {
|
|
199
|
+
const match = /\u001b\[[0-9;]*m/.exec(text.slice(index));
|
|
200
|
+
if (!match) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
output += match[0];
|
|
204
|
+
hasEscape = true;
|
|
205
|
+
index += match[0].length;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (visibleWidth >= maxWidth) {
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
output += text[index];
|
|
214
|
+
visibleWidth += 1;
|
|
215
|
+
index += 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (index < text.length && maxWidth >= 1) {
|
|
219
|
+
output = trimAnsiText(output, maxWidth - 1) + "…";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (hasEscape && !output.endsWith(ANSI_INVERSE_OFF)) {
|
|
223
|
+
output += ANSI_INVERSE_OFF;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return output;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function trimAnsiText(text: string, maxWidth: number): string {
|
|
230
|
+
let visibleWidth = 0;
|
|
231
|
+
let index = 0;
|
|
232
|
+
let output = "";
|
|
233
|
+
|
|
234
|
+
while (index < text.length && visibleWidth < maxWidth) {
|
|
235
|
+
if (text[index] === "\u001b") {
|
|
236
|
+
const match = /\u001b\[[0-9;]*m/.exec(text.slice(index));
|
|
237
|
+
if (!match) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
output += match[0];
|
|
241
|
+
index += match[0].length;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
output += text[index];
|
|
246
|
+
visibleWidth += 1;
|
|
247
|
+
index += 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return output;
|
|
251
|
+
}
|
|
@@ -3,6 +3,9 @@ import type { OpStep, ScenarioSpec } from "../contracts/testing-metadata.types";
|
|
|
3
3
|
import type { SolidTestSpecResult } from "../contracts/test-spec.types";
|
|
4
4
|
|
|
5
5
|
export interface Reporter {
|
|
6
|
+
onRunStart?(args: {
|
|
7
|
+
total: number;
|
|
8
|
+
}): void;
|
|
6
9
|
onScenarioStart(scenario: ScenarioSpec): void;
|
|
7
10
|
onScenarioEnd(
|
|
8
11
|
scenario: ScenarioSpec,
|
|
@@ -67,6 +67,7 @@ export async function runFromMetadata(opts: RunnerOptions): Promise<void> {
|
|
|
67
67
|
|
|
68
68
|
const resources = new SimpleResourceStore();
|
|
69
69
|
const reporter = opts.reporter ?? new ConsoleReporter();
|
|
70
|
+
reporter.onRunStart?.({ total: scenarios.length });
|
|
70
71
|
const api = new ApiAdapter(opts.api);
|
|
71
72
|
const { PlaywrightAdapter } = await import("../adapters/ui/playwright-adapter");
|
|
72
73
|
const ui = new PlaywrightAdapter(opts.ui);
|