@pikku/cli 0.12.89 → 0.12.91
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/README.md +25 -2
- package/console-app/assets/{index-BaOGIr2O.js → index-CpkvmAGb.js} +148 -148
- package/console-app/assets/{index-CSzCJzBb.css → index-D0HG8q0B.css} +1 -1
- package/console-app/index.html +2 -2
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +21 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +6 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +52 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +4 -4
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +73 -13
- package/dist/.pikku/function/pikku-functions.gen.js +9 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +8 -0
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +8 -0
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +4 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.d.ts +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +10 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.json +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +4 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.js +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.d.ts +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +10 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +4 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.js +1 -0
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +9 -1
- package/dist/.pikku/schemas/schemas/DbBaselineInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/DbCheckInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/DbCodegenInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/DbExportInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricValidateInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +19 -0
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +17 -0
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +152 -3
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +39 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/cli.wiring.js +42 -0
- package/dist/src/fabric/fabric-commands.d.ts +9 -3
- package/dist/src/fabric/fabric-commands.js +6 -0
- package/dist/src/fabric/functions/validate.function.d.ts +15 -5
- package/dist/src/fabric/functions/validate.function.js +69 -16
- package/dist/src/fabric/lib/config.d.ts +11 -1
- package/dist/src/fabric/lib/config.js +39 -1
- package/dist/src/fabric/lib/frontend-typecheck.d.ts +46 -0
- package/dist/src/fabric/lib/frontend-typecheck.js +139 -0
- package/dist/src/functions/commands/db-baseline.d.ts +13 -0
- package/dist/src/functions/commands/db-baseline.js +48 -0
- package/dist/src/functions/commands/db-check.d.ts +12 -0
- package/dist/src/functions/commands/db-check.js +64 -0
- package/dist/src/functions/commands/db-codegen.d.ts +11 -0
- package/dist/src/functions/commands/db-codegen.js +39 -0
- package/dist/src/functions/commands/db-export.d.ts +12 -0
- package/dist/src/functions/commands/db-export.js +31 -0
- package/dist/src/functions/commands/db-generate.js +19 -26
- package/dist/src/functions/commands/dev.js +2 -0
- package/dist/src/functions/commands/load-user-project.d.ts +7 -0
- package/dist/src/functions/commands/load-user-project.js +21 -0
- package/dist/src/functions/commands/pikku-command-bootstrap.js +12 -0
- package/dist/src/functions/commands/scenario-browser.d.ts +69 -0
- package/dist/src/functions/commands/scenario-browser.js +60 -0
- package/dist/src/functions/commands/scenario-formatter.d.ts +81 -0
- package/dist/src/functions/commands/scenario-formatter.js +120 -0
- package/dist/src/functions/commands/scenario-ladder.d.ts +66 -0
- package/dist/src/functions/commands/scenario-ladder.js +132 -0
- package/dist/src/functions/commands/scenario-plan.d.ts +61 -0
- package/dist/src/functions/commands/scenario-plan.js +106 -0
- package/dist/src/functions/commands/scenario.d.ts +18 -0
- package/dist/src/functions/commands/scenario.js +305 -38
- package/dist/src/functions/commands/serve.js +2 -0
- package/dist/src/functions/db/db-migrator.d.ts +35 -4
- package/dist/src/functions/db/db-migrator.js +67 -10
- package/dist/src/functions/db/local-db.d.ts +235 -13
- package/dist/src/functions/db/local-db.js +430 -45
- package/dist/src/functions/db/postgres/postgres-introspector.js +19 -1
- package/dist/src/functions/db/postgres/postgres-migrator.d.ts +1 -0
- package/dist/src/functions/db/postgres/postgres-migrator.js +4 -1
- package/dist/src/functions/db/sqlite/sqlite-introspector.js +2 -1
- package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +1 -0
- package/dist/src/functions/db/sqlite/sqlite-migrator.js +6 -1
- package/dist/src/functions/wirings/functions/pikku-command-functions.js +26 -8
- package/dist/src/functions/wirings/functions/serialize-function-types.js +3 -3
- package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +29 -0
- package/dist/src/functions/wirings/scenarios/scenario-partition.js +58 -0
- package/dist/src/functions/wirings/scenarios/serialize-feature-meta.d.ts +14 -0
- package/dist/src/functions/wirings/scenarios/serialize-feature-meta.js +29 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.d.ts +12 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.js +56 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.d.ts +7 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.js +40 -0
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +43 -8
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +1 -1
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +13 -2
- package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.d.ts +4 -0
- package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +62 -0
- package/dist/src/functions/wirings/workflow/serialize-workflow-bootstrap-map.js +10 -3
- package/dist/src/functions/wirings/workflow/serialize-workflow-map.js +10 -3
- package/dist/src/functions/wirings/workflow/serialize-workflow-meta.js +4 -4
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.d.ts +1 -1
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +232 -3
- package/dist/src/server/server-ready.d.ts +12 -0
- package/dist/src/server/server-ready.js +12 -0
- package/dist/src/server/spawn-dev-server.d.ts +50 -0
- package/dist/src/server/spawn-dev-server.js +112 -0
- package/dist/src/services.js +12 -1
- package/dist/src/utils/meta-diff.js +5 -2
- package/dist/src/utils/pikku-cli-config.js +26 -0
- package/dist/src/utils/resolve-scenario-actors.d.ts +23 -0
- package/dist/src/utils/resolve-scenario-actors.js +74 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -6
- package/skills/pikku-i18n/SKILL.md +53 -6
- package/skills/pikku-scenario/SKILL.md +196 -29
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** The default driver, used when a project names no other. */
|
|
2
|
+
export const DEFAULT_BROWSER_DRIVER = '@pikku/playwright';
|
|
3
|
+
/**
|
|
4
|
+
* Both failure modes here are discovered before the first scenario starts. A
|
|
5
|
+
* missing `appUrl` or an uninstalled driver found mid-run costs a whole run to
|
|
6
|
+
* learn about.
|
|
7
|
+
*/
|
|
8
|
+
export const resolveScenarioBrowserProvider = async ({ environment, apiUrl, appUrl, secret, actors, signInPath, failureDir, browserScenarios, driver = DEFAULT_BROWSER_DRIVER, importDriver = (specifier) => import(specifier), }) => {
|
|
9
|
+
if (!appUrl) {
|
|
10
|
+
throw new Error(`Scenario environment '${environment}' has browser steps but no 'appUrl'. ` +
|
|
11
|
+
`Add it to scenarios.environments.${environment} in pikku.config.json, or run with --no-browser to skip them.`);
|
|
12
|
+
}
|
|
13
|
+
const module = await importDriver(driver).catch(() => {
|
|
14
|
+
const install = driver === DEFAULT_BROWSER_DRIVER
|
|
15
|
+
? `Run 'yarn add -D ${driver} @playwright/test'`
|
|
16
|
+
: `Install '${driver}', or point scenarios.browserDriver at a package that is`;
|
|
17
|
+
throw new Error(`Scenarios ${browserScenarios.join(', ')} declare browser steps but '${driver}' could not be loaded. ` +
|
|
18
|
+
`${install}, or run with --no-browser to skip them.`);
|
|
19
|
+
});
|
|
20
|
+
const options = {
|
|
21
|
+
secret,
|
|
22
|
+
actors,
|
|
23
|
+
signInPath,
|
|
24
|
+
failureDir,
|
|
25
|
+
config: module.browserConfigFromEnv?.({ appUrl, apiUrl }) ?? {
|
|
26
|
+
appUrl,
|
|
27
|
+
apiUrl,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
if (module.createScenarioBrowserProvider) {
|
|
31
|
+
return module.createScenarioBrowserProvider(options);
|
|
32
|
+
}
|
|
33
|
+
if (module.PlaywrightScenarioBrowserProvider) {
|
|
34
|
+
return new module.PlaywrightScenarioBrowserProvider(options);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`'${driver}' is not a scenario browser driver: it exports neither ` +
|
|
37
|
+
`'createScenarioBrowserProvider' nor a provider class. A driver returns an object with ` +
|
|
38
|
+
`sessionFor() and close(), and optionally reset() and captureFailure().`);
|
|
39
|
+
};
|
|
40
|
+
export const scenarioBrowserLifecycle = (provider) => ({
|
|
41
|
+
// Deliberately NOT swallowed: a browser that cannot be reset would run the
|
|
42
|
+
// next scenario against the last one's session, and a scenario failing on
|
|
43
|
+
// its reset is far easier to diagnose than one failing on stale state.
|
|
44
|
+
reset: async () => {
|
|
45
|
+
await provider?.reset?.();
|
|
46
|
+
},
|
|
47
|
+
captureFailure: async (label) => {
|
|
48
|
+
try {
|
|
49
|
+
return (await provider?.captureFailure?.(label)) ?? [];
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// This runs while a scenario is already failing. Whatever went wrong
|
|
53
|
+
// here matters less than the error we were called to describe.
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
close: async () => {
|
|
58
|
+
await provider?.close();
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scenario run report, and how it is rendered.
|
|
3
|
+
*
|
|
4
|
+
* Everything a run prints goes through here, so the shape of the output is one
|
|
5
|
+
* file rather than log calls scattered through the command. The report itself
|
|
6
|
+
* is plain serialisable data — no Maps, no meta handles — which is what makes a
|
|
7
|
+
* second formatter (JSON, JUnit) a matter of writing one function rather than
|
|
8
|
+
* unpicking the runner.
|
|
9
|
+
*/
|
|
10
|
+
import type { ScenarioBrowserFailure } from '@pikku/core/workflow';
|
|
11
|
+
/** The longest gherkin keyword ("Given"), so sentences line up under each other. */
|
|
12
|
+
declare const KEYWORD_WIDTH = 5;
|
|
13
|
+
/** One step of a run, already joined to the prose that declared it. */
|
|
14
|
+
export interface ScenarioStepRow {
|
|
15
|
+
sentence: string;
|
|
16
|
+
status: string;
|
|
17
|
+
durationMs?: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Everything known about why one scenario failed. */
|
|
21
|
+
export interface ScenarioFailureDetail {
|
|
22
|
+
/** The rendered sentence of the failing step; absent if no step failed. */
|
|
23
|
+
sentence?: string;
|
|
24
|
+
message: string;
|
|
25
|
+
stack?: string;
|
|
26
|
+
/**
|
|
27
|
+
* True when the failure was a deliberate one (a PikkuError). Its message is
|
|
28
|
+
* the whole story, so the stack is noise.
|
|
29
|
+
*/
|
|
30
|
+
expected?: boolean;
|
|
31
|
+
browser?: ScenarioBrowserFailure[];
|
|
32
|
+
}
|
|
33
|
+
export interface ScenarioResult {
|
|
34
|
+
name: string;
|
|
35
|
+
status: 'passed' | 'failed';
|
|
36
|
+
durationMs: number;
|
|
37
|
+
output?: unknown;
|
|
38
|
+
error?: string;
|
|
39
|
+
steps?: ScenarioStepRow[];
|
|
40
|
+
failure?: ScenarioFailureDetail;
|
|
41
|
+
}
|
|
42
|
+
/** One scenario that was not run, and why. */
|
|
43
|
+
export interface ScenarioSkip {
|
|
44
|
+
name: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ScenarioRunReport {
|
|
48
|
+
environment: string;
|
|
49
|
+
results: ScenarioResult[];
|
|
50
|
+
/**
|
|
51
|
+
* Scenarios not run at all, each carrying why. A skip is only useful if the
|
|
52
|
+
* reader can tell a browser scenario held back by `--no-browser` from one the
|
|
53
|
+
* project itself quarantined, so the reason travels with the name rather than
|
|
54
|
+
* being assumed by the formatter.
|
|
55
|
+
*/
|
|
56
|
+
skipped: ScenarioSkip[];
|
|
57
|
+
/** Feature-level hook failures, which belong to no single scenario. */
|
|
58
|
+
hookFailures: string[];
|
|
59
|
+
}
|
|
60
|
+
export interface ScenarioReportLine {
|
|
61
|
+
level: 'info' | 'error';
|
|
62
|
+
text: string;
|
|
63
|
+
}
|
|
64
|
+
export interface FormatScenarioReportOptions {
|
|
65
|
+
/** Keep every stack frame, including the framework's own. */
|
|
66
|
+
trace?: boolean;
|
|
67
|
+
/** Frames under this directory are the project's own. */
|
|
68
|
+
projectRoot?: string;
|
|
69
|
+
}
|
|
70
|
+
export declare const formatScenarioReport: (report: ScenarioRunReport, options?: FormatScenarioReportOptions) => ScenarioReportLine[];
|
|
71
|
+
export declare const buildStepLadder: (steps: ScenarioStepRow[]) => string[];
|
|
72
|
+
/**
|
|
73
|
+
* The indented block printed under a failed scenario's ladder.
|
|
74
|
+
*
|
|
75
|
+
* One line of `run.error.message` is what a browser step's "Timed out waiting
|
|
76
|
+
* for selector" looks like with all of its context removed. The page's own
|
|
77
|
+
* console errors and failed API calls almost always say why the selector never
|
|
78
|
+
* appeared, and the driver has been collecting them all along.
|
|
79
|
+
*/
|
|
80
|
+
export declare const formatScenarioFailure: (failure: ScenarioFailureDetail, { trace, projectRoot }?: FormatScenarioReportOptions) => string[];
|
|
81
|
+
export { KEYWORD_WIDTH };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** The longest gherkin keyword ("Given"), so sentences line up under each other. */
|
|
2
|
+
const KEYWORD_WIDTH = 5;
|
|
3
|
+
export const formatScenarioReport = (report, options = {}) => {
|
|
4
|
+
const lines = [];
|
|
5
|
+
const info = (text) => lines.push({ level: 'info', text });
|
|
6
|
+
const error = (text) => lines.push({ level: 'error', text });
|
|
7
|
+
for (const { name, reason } of report.skipped) {
|
|
8
|
+
info(`SKIP ${name} (${reason})`);
|
|
9
|
+
}
|
|
10
|
+
for (const result of report.results) {
|
|
11
|
+
if (result.status === 'passed') {
|
|
12
|
+
const output = result.output !== undefined ? ` → ${JSON.stringify(result.output)}` : '';
|
|
13
|
+
info(`PASS ${result.name} (${result.durationMs}ms)${output}`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
error(`FAIL ${result.name} (${result.durationMs}ms): ${firstLine(result.error)}`);
|
|
17
|
+
}
|
|
18
|
+
for (const line of buildStepLadder(result.steps ?? [])) {
|
|
19
|
+
info(line);
|
|
20
|
+
}
|
|
21
|
+
if (result.failure) {
|
|
22
|
+
for (const line of formatScenarioFailure(result.failure, options)) {
|
|
23
|
+
error(line);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const hookFailure of report.hookFailures) {
|
|
28
|
+
error(hookFailure);
|
|
29
|
+
}
|
|
30
|
+
const failed = report.results.filter((r) => r.status === 'failed').length;
|
|
31
|
+
const skippedSuffix = report.skipped.length
|
|
32
|
+
? `, ${report.skipped.length} skipped`
|
|
33
|
+
: '';
|
|
34
|
+
const hookSuffix = report.hookFailures.length
|
|
35
|
+
? `, ${report.hookFailures.length} feature hook failure(s)`
|
|
36
|
+
: '';
|
|
37
|
+
info(`${report.results.length - failed}/${report.results.length} scenarios passed against '${report.environment}'${skippedSuffix}${hookSuffix}`);
|
|
38
|
+
return lines;
|
|
39
|
+
};
|
|
40
|
+
export const buildStepLadder = (steps) => {
|
|
41
|
+
const width = Math.max(0, ...steps.map(({ sentence }) => sentence.length));
|
|
42
|
+
return steps.map((step) => {
|
|
43
|
+
const glyph = step.status === 'succeeded' ? '✓' : '✗';
|
|
44
|
+
const detail = step.status === 'succeeded' || !step.error
|
|
45
|
+
? formatDuration(step.durationMs)
|
|
46
|
+
: firstLine(step.error);
|
|
47
|
+
return ` ${step.sentence.padEnd(width)} ${glyph} ${detail}`;
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The one line a summary gets. A browser timeout's message carries its whole
|
|
52
|
+
* call log; the failure block below prints all of it, so the row above only
|
|
53
|
+
* needs enough to recognise it by.
|
|
54
|
+
*/
|
|
55
|
+
const firstLine = (message) => (message ?? '').split('\n')[0] ?? '';
|
|
56
|
+
/**
|
|
57
|
+
* The indented block printed under a failed scenario's ladder.
|
|
58
|
+
*
|
|
59
|
+
* One line of `run.error.message` is what a browser step's "Timed out waiting
|
|
60
|
+
* for selector" looks like with all of its context removed. The page's own
|
|
61
|
+
* console errors and failed API calls almost always say why the selector never
|
|
62
|
+
* appeared, and the driver has been collecting them all along.
|
|
63
|
+
*/
|
|
64
|
+
export const formatScenarioFailure = (failure, { trace = false, projectRoot } = {}) => {
|
|
65
|
+
const lines = [];
|
|
66
|
+
if (failure.sentence) {
|
|
67
|
+
lines.push(` ✗ failed at: ${failure.sentence}`);
|
|
68
|
+
}
|
|
69
|
+
for (const line of failure.message.split('\n')) {
|
|
70
|
+
lines.push(` ${line}`);
|
|
71
|
+
}
|
|
72
|
+
for (const browser of failure.browser ?? []) {
|
|
73
|
+
lines.push(` browser (${browser.actor})${browser.url ? `: ${browser.url}` : ''}`);
|
|
74
|
+
const detail = (label, values) => {
|
|
75
|
+
for (const value of values) {
|
|
76
|
+
lines.push(` ${`${label}:`.padEnd(11)} ${value}`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
detail('console', browser.consoleErrors);
|
|
80
|
+
detail('page', browser.pageErrors);
|
|
81
|
+
detail('request', browser.failedRequests);
|
|
82
|
+
detail('api', browser.apiErrors);
|
|
83
|
+
if (browser.screenshot) {
|
|
84
|
+
lines.push(` screenshot: ${browser.screenshot}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// An expected failure is a deliberate one — where it was thrown from adds
|
|
88
|
+
// nothing the message and the ladder do not already say.
|
|
89
|
+
if (failure.stack && !failure.expected) {
|
|
90
|
+
for (const frame of stackFrames(failure.stack, trace, projectRoot)) {
|
|
91
|
+
lines.push(` ${frame}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return lines;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* The project's own frames — the ones a reader can act on. Framework and node
|
|
98
|
+
* internals are dropped unless `trace`, or unless dropping them would leave
|
|
99
|
+
* nothing at all: some stack always beats no stack.
|
|
100
|
+
*/
|
|
101
|
+
const stackFrames = (stack, trace, projectRoot) => {
|
|
102
|
+
const frames = stack
|
|
103
|
+
.split('\n')
|
|
104
|
+
.map((line) => line.trim())
|
|
105
|
+
.filter((line) => line.startsWith('at '));
|
|
106
|
+
if (trace || !projectRoot) {
|
|
107
|
+
return frames;
|
|
108
|
+
}
|
|
109
|
+
const own = frames.filter((frame) => frame.includes(projectRoot) && !frame.includes('node_modules'));
|
|
110
|
+
return own.length > 0 ? own : frames;
|
|
111
|
+
};
|
|
112
|
+
const formatDuration = (durationMs) => {
|
|
113
|
+
if (durationMs === undefined) {
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
return durationMs < 1000
|
|
117
|
+
? `${durationMs}ms`
|
|
118
|
+
: `${(durationMs / 1000).toFixed(1)}s`;
|
|
119
|
+
};
|
|
120
|
+
export { KEYWORD_WIDTH };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ScenarioStepPhase } from '@pikku/core/workflow';
|
|
2
|
+
import type { WorkflowStepMeta } from '@pikku/core/workflow/types';
|
|
3
|
+
import type { FunctionsMeta } from '@pikku/core';
|
|
4
|
+
import type { ScenarioFailureDetail, ScenarioStepRow } from './scenario-formatter.js';
|
|
5
|
+
export interface ScenarioStepProse {
|
|
6
|
+
phase: ScenarioStepPhase;
|
|
7
|
+
description: string;
|
|
8
|
+
template?: string;
|
|
9
|
+
actor?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ScenarioStepOutcome {
|
|
12
|
+
stepName: string;
|
|
13
|
+
status: string;
|
|
14
|
+
durationMs?: number;
|
|
15
|
+
error?: string;
|
|
16
|
+
/** The failing error's stack, as recorded on the run. */
|
|
17
|
+
stack?: string;
|
|
18
|
+
/**
|
|
19
|
+
* True when the failure was a deliberate one (a PikkuError). Its message is
|
|
20
|
+
* the whole story, so the stack is noise.
|
|
21
|
+
*/
|
|
22
|
+
expected?: boolean;
|
|
23
|
+
/** The input this step was called with, as recorded on the run. */
|
|
24
|
+
input?: unknown;
|
|
25
|
+
/** The step function that ran it, as recorded on the run. */
|
|
26
|
+
stepFunc?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One scenario's prose, indexed the two ways a run step can be joined back to
|
|
30
|
+
* its declaration.
|
|
31
|
+
*/
|
|
32
|
+
export interface ScenarioProse {
|
|
33
|
+
byStepName: Map<string, ScenarioStepProse>;
|
|
34
|
+
/**
|
|
35
|
+
* The same prose keyed by step function, for steps whose durable name the
|
|
36
|
+
* static meta cannot predict: a step called in a loop is declared as
|
|
37
|
+
* `sees ${packageName}` and reaches the run as `sees @pikku/addon-todos`.
|
|
38
|
+
*
|
|
39
|
+
* A function called from several sites that do not agree on their prose is
|
|
40
|
+
* left out rather than guessed at — such a step falls back to its bare name,
|
|
41
|
+
* which is what it did before this index existed.
|
|
42
|
+
*/
|
|
43
|
+
byStepFunc: Map<string, ScenarioStepProse>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build `durable step name → prose parts` for one scenario.
|
|
47
|
+
*
|
|
48
|
+
* Description precedence matches the runtime: the call site's `description`
|
|
49
|
+
* wins, then the step's own declared `description`, then the positional step
|
|
50
|
+
* name. A step's `template` is carried alongside and, when it exists, is what
|
|
51
|
+
* actually gets rendered — filled from the input recorded on the run.
|
|
52
|
+
*/
|
|
53
|
+
export declare const collectScenarioStepProse: (workflowMeta: {
|
|
54
|
+
steps?: WorkflowStepMeta[];
|
|
55
|
+
} | undefined, functionsMeta: FunctionsMeta) => ScenarioProse;
|
|
56
|
+
/** The step functions in this scenario that declare `browser: true`. */
|
|
57
|
+
export declare const scenarioBrowserSteps: (workflowMeta: {
|
|
58
|
+
steps?: WorkflowStepMeta[];
|
|
59
|
+
} | undefined, functionsMeta: FunctionsMeta) => string[];
|
|
60
|
+
/** The run's steps as renderable rows, in the order they were recorded. */
|
|
61
|
+
export declare const scenarioStepRows: (steps: ScenarioStepOutcome[], prose: ScenarioProse) => ScenarioStepRow[];
|
|
62
|
+
/**
|
|
63
|
+
* Find the step a run died on and join it back to its declared prose, so a
|
|
64
|
+
* failure names the sentence a reader recognises rather than a durable key.
|
|
65
|
+
*/
|
|
66
|
+
export declare const scenarioFailureFromSteps: (steps: ScenarioStepOutcome[], prose: ScenarioProse) => ScenarioFailureDetail | undefined;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scenario step ladder — joining a recorded run back to the English that
|
|
3
|
+
* declared it.
|
|
4
|
+
*
|
|
5
|
+
* Cucumber parsed English into calls; this recovers English out of the typed
|
|
6
|
+
* calls the inspector already recorded, joined against the run the engine
|
|
7
|
+
* already persisted. No engine change, no step-event bus. Layout of the result
|
|
8
|
+
* belongs to `scenario-formatter.ts`; this module only decides what each step
|
|
9
|
+
* is called.
|
|
10
|
+
*/
|
|
11
|
+
import { composeStepProse } from '@pikku/core/workflow';
|
|
12
|
+
import { KEYWORD_WIDTH } from './scenario-formatter.js';
|
|
13
|
+
/**
|
|
14
|
+
* Walk a workflow's steps — including the ones nested inside branches, fanouts
|
|
15
|
+
* and parallel groups — and yield every scenario step it declares.
|
|
16
|
+
*/
|
|
17
|
+
function* walkScenarioSteps(steps) {
|
|
18
|
+
if (!Array.isArray(steps)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const step of steps) {
|
|
22
|
+
const node = step;
|
|
23
|
+
if (node?.type === 'scenarioStep') {
|
|
24
|
+
yield node;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
yield* walkScenarioSteps(node?.children);
|
|
28
|
+
yield* walkScenarioSteps(node?.body);
|
|
29
|
+
yield* walkScenarioSteps(node?.steps);
|
|
30
|
+
yield* walkScenarioSteps(node?.then);
|
|
31
|
+
yield* walkScenarioSteps(node?.else);
|
|
32
|
+
for (const branch of Object.values(node?.cases ?? {})) {
|
|
33
|
+
yield* walkScenarioSteps(branch);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build `durable step name → prose parts` for one scenario.
|
|
39
|
+
*
|
|
40
|
+
* Description precedence matches the runtime: the call site's `description`
|
|
41
|
+
* wins, then the step's own declared `description`, then the positional step
|
|
42
|
+
* name. A step's `template` is carried alongside and, when it exists, is what
|
|
43
|
+
* actually gets rendered — filled from the input recorded on the run.
|
|
44
|
+
*/
|
|
45
|
+
export const collectScenarioStepProse = (workflowMeta, functionsMeta) => {
|
|
46
|
+
const byStepName = new Map();
|
|
47
|
+
const byStepFunc = new Map();
|
|
48
|
+
const ambiguous = new Set();
|
|
49
|
+
for (const step of walkScenarioSteps(workflowMeta?.steps)) {
|
|
50
|
+
const stepMeta = functionsMeta[step.stepFunc];
|
|
51
|
+
const parts = {
|
|
52
|
+
phase: step.phase ?? 'step',
|
|
53
|
+
description: step.options?.description ?? stepMeta?.description ?? step.stepName,
|
|
54
|
+
// A call-site description is an explicit override, so it wins over the
|
|
55
|
+
// step's template the same way it wins over its description.
|
|
56
|
+
template: step.options?.description
|
|
57
|
+
? undefined
|
|
58
|
+
: stepMeta?.scenarioStepTemplate,
|
|
59
|
+
actor: step.actor,
|
|
60
|
+
};
|
|
61
|
+
byStepName.set(step.stepName, parts);
|
|
62
|
+
const declared = byStepFunc.get(step.stepFunc);
|
|
63
|
+
if (declared && !sameProse(declared, parts)) {
|
|
64
|
+
ambiguous.add(step.stepFunc);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
byStepFunc.set(step.stepFunc, parts);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const stepFunc of ambiguous) {
|
|
71
|
+
byStepFunc.delete(stepFunc);
|
|
72
|
+
}
|
|
73
|
+
return { byStepName, byStepFunc };
|
|
74
|
+
};
|
|
75
|
+
const sameProse = (a, b) => a.phase === b.phase &&
|
|
76
|
+
a.description === b.description &&
|
|
77
|
+
a.template === b.template &&
|
|
78
|
+
a.actor === b.actor;
|
|
79
|
+
/** The step functions in this scenario that declare `browser: true`. */
|
|
80
|
+
export const scenarioBrowserSteps = (workflowMeta, functionsMeta) => {
|
|
81
|
+
const names = new Set();
|
|
82
|
+
for (const step of walkScenarioSteps(workflowMeta?.steps)) {
|
|
83
|
+
if (functionsMeta[step.stepFunc]?.scenarioStepBrowser === true) {
|
|
84
|
+
names.add(step.stepFunc);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...names];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Render one step's gherkin sentence.
|
|
91
|
+
*
|
|
92
|
+
* A repeated step is stored as `name#1`; its prose lives under the bare name.
|
|
93
|
+
* Only once neither name matches does the step function decide it, so a step
|
|
94
|
+
* recorded under the name it was declared with is never re-resolved by a
|
|
95
|
+
* function shared with another call site.
|
|
96
|
+
*/
|
|
97
|
+
const stepSentence = (step, prose) => {
|
|
98
|
+
const parts = prose.byStepName.get(step.stepName) ??
|
|
99
|
+
prose.byStepName.get(baseName(step.stepName)) ??
|
|
100
|
+
(step.stepFunc ? prose.byStepFunc.get(step.stepFunc) : undefined);
|
|
101
|
+
return parts
|
|
102
|
+
? composeStepProse({
|
|
103
|
+
...parts,
|
|
104
|
+
input: step.input,
|
|
105
|
+
keywordWidth: KEYWORD_WIDTH,
|
|
106
|
+
})
|
|
107
|
+
: `${''.padEnd(KEYWORD_WIDTH)} ${step.stepName}`;
|
|
108
|
+
};
|
|
109
|
+
/** The run's steps as renderable rows, in the order they were recorded. */
|
|
110
|
+
export const scenarioStepRows = (steps, prose) => steps.map((step) => ({
|
|
111
|
+
sentence: stepSentence(step, prose),
|
|
112
|
+
status: step.status,
|
|
113
|
+
durationMs: step.durationMs,
|
|
114
|
+
error: step.error,
|
|
115
|
+
}));
|
|
116
|
+
const baseName = (stepName) => stepName.replace(/#\d+$/, '');
|
|
117
|
+
/**
|
|
118
|
+
* Find the step a run died on and join it back to its declared prose, so a
|
|
119
|
+
* failure names the sentence a reader recognises rather than a durable key.
|
|
120
|
+
*/
|
|
121
|
+
export const scenarioFailureFromSteps = (steps, prose) => {
|
|
122
|
+
const failed = steps.find((step) => step.status !== 'succeeded');
|
|
123
|
+
if (!failed) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
sentence: stepSentence(failed, prose).trimEnd(),
|
|
128
|
+
message: failed.error ?? `step status: ${failed.status}`,
|
|
129
|
+
stack: failed.stack,
|
|
130
|
+
expected: failed.expected,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CoreFeature, CoreWorkflow } from '@pikku/core/workflow';
|
|
2
|
+
/** One scenario run: a scenario name and, for a feature's paired entry, its input. */
|
|
3
|
+
export type ScenarioPlanEntry = {
|
|
4
|
+
scenarioName: string;
|
|
5
|
+
data?: unknown;
|
|
6
|
+
tags: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Why this scenario is not part of a default run. It stays in the plan so
|
|
9
|
+
* the runner reports it as skipped rather than omitting it silently, and it
|
|
10
|
+
* is cleared when the scenario is named directly with `--flows` — the
|
|
11
|
+
* explicit ask that overrides the quarantine.
|
|
12
|
+
*/
|
|
13
|
+
skip?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A run unit. A feature is one group whose hooks run once around all of its
|
|
17
|
+
* entries; a scenario belonging to no feature is a group of one with no hooks.
|
|
18
|
+
*/
|
|
19
|
+
export type ScenarioPlanGroup = {
|
|
20
|
+
featureId?: string;
|
|
21
|
+
featureName?: string;
|
|
22
|
+
before?: CoreFeature['before'];
|
|
23
|
+
after?: CoreFeature['after'];
|
|
24
|
+
entries: ScenarioPlanEntry[];
|
|
25
|
+
};
|
|
26
|
+
export type ScenarioPlanInput = {
|
|
27
|
+
/** Scenarios the inspector found, in declaration order. */
|
|
28
|
+
scenarios: Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
tags: string[];
|
|
31
|
+
skip?: string;
|
|
32
|
+
}>;
|
|
33
|
+
features: Map<string, CoreFeature>;
|
|
34
|
+
registrations: Map<string, CoreWorkflow>;
|
|
35
|
+
flows?: string[];
|
|
36
|
+
featureIds?: string[];
|
|
37
|
+
tags?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Tags that disqualify a scenario. `tags` is match-any with no negation, so
|
|
40
|
+
* without this a default run cannot express "everything except the
|
|
41
|
+
* live-model ones" — the shape every suite that holds a cluster back needs.
|
|
42
|
+
*/
|
|
43
|
+
excludeTags?: string[];
|
|
44
|
+
};
|
|
45
|
+
export type ScenarioPlan = {
|
|
46
|
+
groups: ScenarioPlanGroup[];
|
|
47
|
+
/** Feature entries whose scenario is not registered — reported, never skipped silently. */
|
|
48
|
+
unresolved: Array<{
|
|
49
|
+
featureId: string;
|
|
50
|
+
index: number;
|
|
51
|
+
}>;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Turn the registered scenarios and features into an ordered list of run units.
|
|
55
|
+
*
|
|
56
|
+
* Features come first, in registration order, then the scenarios that belong to
|
|
57
|
+
* no feature, in declaration order. Selection is a filter over that plan rather
|
|
58
|
+
* than a different traversal, so `--tags` narrowing a feature to two of its
|
|
59
|
+
* five scenarios still runs the feature's hooks exactly once around those two.
|
|
60
|
+
*/
|
|
61
|
+
export declare const buildScenarioPlan: ({ scenarios, features, registrations, flows, featureIds, tags, excludeTags, }: ScenarioPlanInput) => ScenarioPlan;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { resolveFeatureScenarios } from '@pikku/core/workflow';
|
|
2
|
+
/**
|
|
3
|
+
* Turn the registered scenarios and features into an ordered list of run units.
|
|
4
|
+
*
|
|
5
|
+
* Features come first, in registration order, then the scenarios that belong to
|
|
6
|
+
* no feature, in declaration order. Selection is a filter over that plan rather
|
|
7
|
+
* than a different traversal, so `--tags` narrowing a feature to two of its
|
|
8
|
+
* five scenarios still runs the feature's hooks exactly once around those two.
|
|
9
|
+
*/
|
|
10
|
+
export const buildScenarioPlan = ({ scenarios, features, registrations, flows, featureIds, tags, excludeTags, }) => {
|
|
11
|
+
const { entries, unresolved } = resolveFeatureScenarios(features, registrations);
|
|
12
|
+
if (flows) {
|
|
13
|
+
const known = new Set(scenarios.map((s) => s.name));
|
|
14
|
+
const unknown = flows.filter((name) => !known.has(name));
|
|
15
|
+
if (unknown.length > 0) {
|
|
16
|
+
throw new Error(`Unknown scenario(s): ${unknown.join(', ')}. Available: ${scenarios
|
|
17
|
+
.map((s) => s.name)
|
|
18
|
+
.join(', ')}`);
|
|
19
|
+
}
|
|
20
|
+
// A scenario every one of whose feature entries carries `data` has no input
|
|
21
|
+
// to run with when it is named directly — the feature is what supplies it,
|
|
22
|
+
// so the feature is what runs. A scenario referenced bare anywhere, or in no
|
|
23
|
+
// feature at all, runs standalone exactly as before.
|
|
24
|
+
for (const name of flows) {
|
|
25
|
+
const appearances = entries.filter((e) => e.scenarioName === name);
|
|
26
|
+
if (appearances.length === 0)
|
|
27
|
+
continue;
|
|
28
|
+
if (appearances.some((e) => e.data === undefined))
|
|
29
|
+
continue;
|
|
30
|
+
const containing = [...new Set(appearances.map((e) => e.featureId))];
|
|
31
|
+
throw new Error(`Scenario '${name}' is only ever run with data supplied by a feature, so there is nothing to run it with on its own — run the feature instead: --features ${containing.join(',')}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (featureIds) {
|
|
35
|
+
const unknown = featureIds.filter((id) => !features.has(id));
|
|
36
|
+
if (unknown.length > 0) {
|
|
37
|
+
throw new Error(`Unknown feature(s): ${unknown.join(', ')}. Available: ${[...features.keys()].join(', ') || '(none)'}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const skipReasons = new Map(scenarios
|
|
41
|
+
.filter((scenario) => scenario.skip)
|
|
42
|
+
.map((scenario) => [scenario.name, scenario.skip]));
|
|
43
|
+
const wantedFlows = flows ? new Set(flows) : undefined;
|
|
44
|
+
const wantedFeatures = featureIds ? new Set(featureIds) : undefined;
|
|
45
|
+
const wantedTags = tags ? new Set(tags) : undefined;
|
|
46
|
+
const unwantedTags = excludeTags ? new Set(excludeTags) : undefined;
|
|
47
|
+
// Naming a scenario with `--flows` is the explicit ask that overrides its
|
|
48
|
+
// quarantine; narrowing to its feature is not — a feature is a group, and
|
|
49
|
+
// running the group should not silently drag a quarantined member in.
|
|
50
|
+
const skipFor = (scenarioName) => wantedFlows?.has(scenarioName) ? undefined : skipReasons.get(scenarioName);
|
|
51
|
+
const keep = (entry, featureId) => {
|
|
52
|
+
if (wantedFeatures && !(featureId && wantedFeatures.has(featureId))) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (wantedFlows && !wantedFlows.has(entry.scenarioName)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (wantedTags && !entry.tags.some((tag) => wantedTags.has(tag))) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// Naming a scenario with `--flows` is the explicit ask, the same override
|
|
62
|
+
// that lifts a quarantine: it outranks an exclusion the caller did not aim
|
|
63
|
+
// at this scenario in particular.
|
|
64
|
+
if (unwantedTags &&
|
|
65
|
+
!wantedFlows?.has(entry.scenarioName) &&
|
|
66
|
+
entry.tags.some((tag) => unwantedTags.has(tag))) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
};
|
|
71
|
+
const groups = [];
|
|
72
|
+
for (const [featureId, feature] of features) {
|
|
73
|
+
const kept = entries
|
|
74
|
+
.filter((entry) => entry.featureId === featureId)
|
|
75
|
+
.map(({ scenarioName, data, tags: entryTags }) => ({
|
|
76
|
+
scenarioName,
|
|
77
|
+
data,
|
|
78
|
+
tags: entryTags,
|
|
79
|
+
skip: skipFor(scenarioName),
|
|
80
|
+
}))
|
|
81
|
+
.filter((entry) => keep(entry, featureId));
|
|
82
|
+
if (kept.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
groups.push({
|
|
85
|
+
featureId,
|
|
86
|
+
featureName: feature.name ?? featureId,
|
|
87
|
+
before: feature.before,
|
|
88
|
+
after: feature.after,
|
|
89
|
+
entries: kept,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const inAFeature = new Set(entries.map((entry) => entry.scenarioName));
|
|
93
|
+
for (const scenario of scenarios) {
|
|
94
|
+
if (inAFeature.has(scenario.name))
|
|
95
|
+
continue;
|
|
96
|
+
const entry = {
|
|
97
|
+
scenarioName: scenario.name,
|
|
98
|
+
tags: scenario.tags,
|
|
99
|
+
skip: skipFor(scenario.name),
|
|
100
|
+
};
|
|
101
|
+
if (!keep(entry))
|
|
102
|
+
continue;
|
|
103
|
+
groups.push({ entries: [entry] });
|
|
104
|
+
}
|
|
105
|
+
return { groups, unresolved };
|
|
106
|
+
};
|
|
@@ -2,16 +2,34 @@ export declare const scenarioList: import("#pikku").PikkuFunctionConfig<{}, void
|
|
|
2
2
|
export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
|
|
3
3
|
environment: string;
|
|
4
4
|
flows?: string;
|
|
5
|
+
features?: string;
|
|
5
6
|
tags?: string;
|
|
7
|
+
excludeTags?: string;
|
|
6
8
|
coverage?: boolean;
|
|
9
|
+
browser?: boolean;
|
|
10
|
+
spawn?: boolean;
|
|
11
|
+
keepAlive?: boolean;
|
|
12
|
+
trace?: boolean;
|
|
7
13
|
}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
|
|
8
14
|
environment: string;
|
|
9
15
|
flows?: string;
|
|
16
|
+
features?: string;
|
|
10
17
|
tags?: string;
|
|
18
|
+
excludeTags?: string;
|
|
11
19
|
coverage?: boolean;
|
|
20
|
+
browser?: boolean;
|
|
21
|
+
spawn?: boolean;
|
|
22
|
+
keepAlive?: boolean;
|
|
23
|
+
trace?: boolean;
|
|
12
24
|
}, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{
|
|
13
25
|
environment: string;
|
|
14
26
|
flows?: string;
|
|
27
|
+
features?: string;
|
|
15
28
|
tags?: string;
|
|
29
|
+
excludeTags?: string;
|
|
16
30
|
coverage?: boolean;
|
|
31
|
+
browser?: boolean;
|
|
32
|
+
spawn?: boolean;
|
|
33
|
+
keepAlive?: boolean;
|
|
34
|
+
trace?: boolean;
|
|
17
35
|
}, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
|