@itmetsam/samurai-framework 0.22.4 → 0.22.6
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/agent/generate-kind.d.ts +5 -0
- package/dist/agent/generate-kind.js +45 -0
- package/dist/agent/index.d.ts +13 -3
- package/dist/agent/index.js +15 -2
- package/dist/agent/plan/front-matter.d.ts +9 -0
- package/dist/agent/plan/front-matter.js +18 -0
- package/dist/agent/plan/guard.d.ts +7 -0
- package/dist/agent/plan/guard.js +32 -0
- package/dist/agent/plan/plan-file.d.ts +67 -0
- package/dist/agent/plan/plan-file.js +229 -0
- package/dist/agent/plan/plan-kind.d.ts +5 -0
- package/dist/agent/plan/plan-kind.js +28 -0
- package/dist/agent/plan/plan-path.d.ts +8 -0
- package/dist/agent/plan/plan-path.js +46 -0
- package/dist/agent/plan/prompt.d.ts +3 -0
- package/dist/agent/plan/prompt.js +37 -0
- package/dist/agent/plan/questions.d.ts +5 -0
- package/dist/agent/plan/questions.js +22 -0
- package/dist/agent/plan/read-plan.d.ts +5 -0
- package/dist/agent/plan/read-plan.js +29 -0
- package/dist/agent/plan/recorder.d.ts +30 -0
- package/dist/agent/plan/recorder.js +59 -0
- package/dist/agent/plan/run.d.ts +8 -0
- package/dist/agent/plan/run.js +10 -0
- package/dist/agent/plan/tools.d.ts +4 -0
- package/dist/agent/plan/tools.js +95 -0
- package/dist/agent/plan-link.d.ts +8 -0
- package/dist/agent/plan-link.js +25 -0
- package/dist/agent/prompt.d.ts +6 -0
- package/dist/agent/prompt.js +20 -6
- package/dist/agent/recording.d.ts +6 -0
- package/dist/agent/recording.js +14 -0
- package/dist/agent/run-kind.d.ts +48 -0
- package/dist/agent/run-kind.js +1 -0
- package/dist/agent/run.d.ts +7 -3
- package/dist/agent/run.js +19 -27
- package/dist/agent/sdk-tools.js +10 -2
- package/dist/agent/tools/context.d.ts +12 -5
- package/dist/agent/tools/index.js +2 -0
- package/dist/agent/tools/undo.d.ts +1 -0
- package/dist/agent/tools/undo.js +23 -0
- package/dist/agent/types.d.ts +36 -7
- package/dist/cli/agent-run.d.ts +43 -0
- package/dist/cli/agent-run.js +132 -0
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +3 -0
- package/dist/cli/generate.d.ts +4 -8
- package/dist/cli/generate.js +18 -109
- package/dist/cli/main.js +3 -0
- package/dist/cli/plan.d.ts +6 -0
- package/dist/cli/plan.js +31 -0
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +9 -1
- package/dist/config/config.js +2 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +2 -0
- package/dist/config/plans-dir.d.ts +3 -0
- package/dist/config/plans-dir.js +11 -0
- package/dist/config/resolved-config.d.ts +1 -0
- package/dist/config/resolved-config.js +2 -1
- package/dist/recorder/spec-source.d.ts +2 -0
- package/dist/recorder/spec-source.js +9 -0
- package/dist/runner/registry.js +6 -2
- package/dist/runner/test-info.js +1 -0
- package/dist/runner/test-io.d.ts +5 -1
- package/dist/runner/test-io.js +21 -0
- package/dist/steps/edit.d.ts +3 -1
- package/dist/steps/edit.js +1 -1
- package/dist/steps/model.d.ts +3 -1
- package/dist/steps/parse.js +2 -1
- package/dist/steps/test-options.d.ts +5 -3
- package/dist/steps/test-options.js +49 -0
- package/dist/types/config.d.ts +6 -0
- package/dist/types/test.d.ts +4 -0
- package/package.json +1 -1
package/dist/config/config.js
CHANGED
|
@@ -8,6 +8,7 @@ import { validateForbidOnlyConfig } from "./forbid-only.js";
|
|
|
8
8
|
import { validateRetriesConfig } from "./retries.js";
|
|
9
9
|
import { validateFullyParallelConfig } from "./fully-parallel.js";
|
|
10
10
|
import { validateWorkersConfig } from "./workers.js";
|
|
11
|
+
import { validatePlansDirConfig } from "./plans-dir.js";
|
|
11
12
|
import { validateWebServerConfig } from "./web-server-options.js";
|
|
12
13
|
let activeProject;
|
|
13
14
|
/** Makes `project` the one config, secrets, reports and downloads are resolved for; `undefined` ends it */
|
|
@@ -39,6 +40,7 @@ export function validateConfig(config) {
|
|
|
39
40
|
validateGroupUse(config);
|
|
40
41
|
validateEnvironmentUse(config);
|
|
41
42
|
validateWebServerConfig(config);
|
|
43
|
+
validatePlansDirConfig(config);
|
|
42
44
|
}
|
|
43
45
|
/** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
|
|
44
46
|
export async function loadRawConfig() {
|
|
@@ -33,6 +33,8 @@ export declare const DEFAULT_VIEWPORT: {
|
|
|
33
33
|
export declare const DEFAULT_DOWNLOADS_DIR = "result/downloads";
|
|
34
34
|
/** The files folder, relative to the project */
|
|
35
35
|
export declare const DEFAULT_FILES_DIR = "files";
|
|
36
|
+
/** Where test plans are written, relative to the project */
|
|
37
|
+
export declare const DEFAULT_PLANS_DIR = "plans";
|
|
36
38
|
/** How many times a failing test is run again */
|
|
37
39
|
export declare const DEFAULT_RETRIES = 0;
|
|
38
40
|
/** Which page log entries go in the report: those of failed tests */
|
package/dist/config/defaults.js
CHANGED
|
@@ -30,6 +30,8 @@ export const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
|
30
30
|
export const DEFAULT_DOWNLOADS_DIR = "result/downloads";
|
|
31
31
|
/** The files folder, relative to the project */
|
|
32
32
|
export const DEFAULT_FILES_DIR = "files";
|
|
33
|
+
/** Where test plans are written, relative to the project */
|
|
34
|
+
export const DEFAULT_PLANS_DIR = "plans";
|
|
33
35
|
/** How many times a failing test is run again */
|
|
34
36
|
export const DEFAULT_RETRIES = 0;
|
|
35
37
|
/** Which page log entries go in the report: those of failed tests */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/** Checks the `plansDir` key of a config: a folder inside the project. Throws an Error that names the key */
|
|
3
|
+
export function validatePlansDirConfig(config) {
|
|
4
|
+
const dir = config.plansDir;
|
|
5
|
+
if (dir === undefined)
|
|
6
|
+
return;
|
|
7
|
+
if (typeof dir !== "string" || dir.trim() === "")
|
|
8
|
+
throw new Error(`plansDir must be a folder name relative to the project, got ${JSON.stringify(dir)}`);
|
|
9
|
+
if (path.isAbsolute(dir) || path.normalize(dir).split(path.sep)[0] === "..")
|
|
10
|
+
throw new Error(`plansDir must be a folder inside the project, got "${dir}"`);
|
|
11
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { browsersFor } from "./browsers-for.js";
|
|
3
3
|
import { importConfigFile, projectConfig, validateConfig } from "./config.js";
|
|
4
|
-
import { DEFAULT_COLLECT_BODIES, DEFAULT_COMMAND_TIMEOUT, DEFAULT_DOWNLOADS_DIR, DEFAULT_FAIL_ON_PAGE_ERROR, DEFAULT_FILES_DIR, DEFAULT_IDLE_TIME, DEFAULT_LOGS_CAPTURE, DEFAULT_MAX_BODY_SIZE, DEFAULT_NAVIGATION_TIMEOUT, DEFAULT_RETRIES, DEFAULT_ROUTE_TIMEOUT, DEFAULT_SRC_DIR, DEFAULT_TRACK_NETWORK, DEFAULT_VIEWPORT, DEFAULT_WAIT_UNTIL, } from "./defaults.js";
|
|
4
|
+
import { DEFAULT_COLLECT_BODIES, DEFAULT_COMMAND_TIMEOUT, DEFAULT_DOWNLOADS_DIR, DEFAULT_FAIL_ON_PAGE_ERROR, DEFAULT_FILES_DIR, DEFAULT_PLANS_DIR, DEFAULT_IDLE_TIME, DEFAULT_LOGS_CAPTURE, DEFAULT_MAX_BODY_SIZE, DEFAULT_NAVIGATION_TIMEOUT, DEFAULT_RETRIES, DEFAULT_ROUTE_TIMEOUT, DEFAULT_SRC_DIR, DEFAULT_TRACK_NETWORK, DEFAULT_VIEWPORT, DEFAULT_WAIT_UNTIL, } from "./defaults.js";
|
|
5
5
|
import { forbidsOnly } from "./forbid-only.js";
|
|
6
6
|
import { IMPLICIT_ENVIRONMENT, resolveRunSettings, UnknownEnvironmentError, } from "./run-settings.js";
|
|
7
7
|
import { resolveWorkers } from "./workers-option.js";
|
|
@@ -72,6 +72,7 @@ export function resolveConfig(config, options) {
|
|
|
72
72
|
viewport: viewport === null ? null : { ...(viewport ?? DEFAULT_VIEWPORT) },
|
|
73
73
|
downloadsDir: config.downloadsDir ?? DEFAULT_DOWNLOADS_DIR,
|
|
74
74
|
filesDir: config.filesDir ?? DEFAULT_FILES_DIR,
|
|
75
|
+
plansDir: config.plansDir ?? DEFAULT_PLANS_DIR,
|
|
75
76
|
logs: {
|
|
76
77
|
capture: config.logs?.capture ?? DEFAULT_LOGS_CAPTURE,
|
|
77
78
|
failOnPageError: config.logs?.failOnPageError ?? DEFAULT_FAIL_ON_PAGE_ERROR,
|
|
@@ -34,6 +34,8 @@ export declare class SpecWriter {
|
|
|
34
34
|
private readonly testIndex;
|
|
35
35
|
readonly events: RecorderEvent[];
|
|
36
36
|
constructor(source: string, file: string, testIndex: number);
|
|
37
|
+
/** Changes the source with `change` and writes the file when it differs; returns the new content */
|
|
38
|
+
rewrite(change: (source: string) => string): string;
|
|
37
39
|
/** Applies the event, writes the file and returns the file's new content */
|
|
38
40
|
write(event: RecorderEvent): string;
|
|
39
41
|
}
|
|
@@ -74,6 +74,15 @@ export class SpecWriter {
|
|
|
74
74
|
this.testIndex = testIndex;
|
|
75
75
|
this.events = [];
|
|
76
76
|
}
|
|
77
|
+
/** Changes the source with `change` and writes the file when it differs; returns the new content */
|
|
78
|
+
rewrite(change) {
|
|
79
|
+
const next = change(this.source);
|
|
80
|
+
if (next !== this.source) {
|
|
81
|
+
this.source = next;
|
|
82
|
+
writeFileSync(this.file, this.source);
|
|
83
|
+
}
|
|
84
|
+
return this.source;
|
|
85
|
+
}
|
|
77
86
|
/** Applies the event, writes the file and returns the file's new content */
|
|
78
87
|
write(event) {
|
|
79
88
|
this.source = applyRecorderEvent(this.source, this.testIndex, event);
|
package/dist/runner/registry.js
CHANGED
|
@@ -7,7 +7,7 @@ import { validateUseOptions } from "../config/use-options.js";
|
|
|
7
7
|
import { currentStepLog, currentTestInfo, infoFor, skipFromCondition, } from "./test-info.js";
|
|
8
8
|
import { runStep } from "./steps.js";
|
|
9
9
|
import { markTests } from "./test-annotations.js";
|
|
10
|
-
import { validateTestIO } from "./test-io.js";
|
|
10
|
+
import { validateTestAnnotations, validateTestIO } from "./test-io.js";
|
|
11
11
|
import { addHook, addUse, currentSuite, enterSuite, leaveSuite, modifierOf, resetSuites, } from "./suite.js";
|
|
12
12
|
import { configureSuite } from "./serial.js";
|
|
13
13
|
const registered = [];
|
|
@@ -26,7 +26,7 @@ function openDescribe(file, title, fn, modifier, reason, mode) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
function add({ file, line, column }, title, fn, modifier, extras = {}) {
|
|
29
|
-
const { reason, inputs, outputs, extension, expectedFail } = extras;
|
|
29
|
+
const { reason, inputs, outputs, annotations, extension, expectedFail } = extras;
|
|
30
30
|
const suite = currentSuite(file);
|
|
31
31
|
const titlePath = [...titlesOf(suite), title];
|
|
32
32
|
const name = titlePath.join(" > ");
|
|
@@ -40,6 +40,7 @@ function add({ file, line, column }, title, fn, modifier, extras = {}) {
|
|
|
40
40
|
...modifierOf({ modifier, ...(reason !== undefined && { reason }) }, suite),
|
|
41
41
|
...(inputs && { inputs }),
|
|
42
42
|
...(outputs && { outputs }),
|
|
43
|
+
...(annotations && { annotations }),
|
|
43
44
|
...(extension && { extension }),
|
|
44
45
|
...(expectedFail && { expectedFail }),
|
|
45
46
|
});
|
|
@@ -65,6 +66,9 @@ function bodyAndOptions(call, title, second, third) {
|
|
|
65
66
|
fn: fn,
|
|
66
67
|
...(reason !== undefined && { reason }),
|
|
67
68
|
...(options && call.startsWith("test") && validateTestIO(call, options)),
|
|
69
|
+
...(options &&
|
|
70
|
+
call.startsWith("test") &&
|
|
71
|
+
validateTestAnnotations(call, options)),
|
|
68
72
|
};
|
|
69
73
|
}
|
|
70
74
|
/** `test()` and `test.only()`: a reason only makes sense with skip and fixme */
|
package/dist/runner/test-info.js
CHANGED
|
@@ -19,6 +19,7 @@ export class TestInfoImpl {
|
|
|
19
19
|
const { test } = from;
|
|
20
20
|
this.title = test.titlePath[test.titlePath.length - 1] ?? test.name;
|
|
21
21
|
this.titlePath = [...test.titlePath];
|
|
22
|
+
this.annotations = (test.annotations ?? []).map((a) => ({ ...a }));
|
|
22
23
|
this.file = test.file;
|
|
23
24
|
if (test.line !== undefined)
|
|
24
25
|
this.line = test.line;
|
package/dist/runner/test-io.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TestError, TestOptions, TestOutputs } from "../types/test.js";
|
|
1
|
+
import type { TestAnnotation, TestError, TestOptions, TestOutputs } from "../types/test.js";
|
|
2
2
|
import type { OutputSink } from "../wait/output-sink.js";
|
|
3
3
|
/**
|
|
4
4
|
* Checks the `inputs` and `outputs` of a `test()` options object and returns them, leaving out an empty or absent
|
|
@@ -8,6 +8,10 @@ export declare function validateTestIO(call: string, options: TestOptions): {
|
|
|
8
8
|
inputs?: Record<string, string>;
|
|
9
9
|
outputs?: string[];
|
|
10
10
|
};
|
|
11
|
+
/** Checks the `annotations` of a `test()` options object and returns them, or nothing when absent or empty */
|
|
12
|
+
export declare function validateTestAnnotations(call: string, options: TestOptions): {
|
|
13
|
+
annotations?: TestAnnotation[];
|
|
14
|
+
};
|
|
11
15
|
/**
|
|
12
16
|
* The `inputs` fixture: each declared input with the value the run supplies, else its default. Reading a
|
|
13
17
|
* name the test did not declare throws.
|
package/dist/runner/test-io.js
CHANGED
|
@@ -45,6 +45,27 @@ export function validateTestIO(call, options) {
|
|
|
45
45
|
}
|
|
46
46
|
return result;
|
|
47
47
|
}
|
|
48
|
+
/** Checks the `annotations` of a `test()` options object and returns them, or nothing when absent or empty */
|
|
49
|
+
export function validateTestAnnotations(call, options) {
|
|
50
|
+
const { annotations } = options;
|
|
51
|
+
if (annotations === undefined)
|
|
52
|
+
return {};
|
|
53
|
+
if (!Array.isArray(annotations))
|
|
54
|
+
throw new Error(`${call}(): annotations must be an array`);
|
|
55
|
+
for (const note of annotations) {
|
|
56
|
+
const entry = note;
|
|
57
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
58
|
+
throw new Error(`${call}(): each annotation must be an object with a type`);
|
|
59
|
+
if (typeof entry.type !== "string" || entry.type === "")
|
|
60
|
+
throw new Error(`${call}(): an annotation needs a non-empty type`);
|
|
61
|
+
if (entry.description !== undefined &&
|
|
62
|
+
typeof entry.description !== "string")
|
|
63
|
+
throw new Error(`${call}(): the description of annotation "${entry.type}" must be a string`);
|
|
64
|
+
}
|
|
65
|
+
return annotations.length > 0
|
|
66
|
+
? { annotations: annotations.map((a) => ({ ...a })) }
|
|
67
|
+
: {};
|
|
68
|
+
}
|
|
48
69
|
/**
|
|
49
70
|
* The `inputs` fixture: each declared input with the value the run supplies, else its default. Reading a
|
|
50
71
|
* name the test did not declare throws.
|
package/dist/steps/edit.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TestAnnotation } from "../types/test.js";
|
|
1
2
|
import type { Step, TestInput } from "./model.js";
|
|
2
3
|
export type StepEdit = {
|
|
3
4
|
op: "replace";
|
|
@@ -34,7 +35,7 @@ export type StepEdit = {
|
|
|
34
35
|
test: number;
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
|
-
* Sets the test's `inputs` and `
|
|
38
|
+
* Sets the test's `inputs`, `outputs` and `annotations` in its options object: a given list replaces that list, one left out
|
|
38
39
|
* stays. When both end up empty the options object goes, unless it holds a `reason`
|
|
39
40
|
*/
|
|
40
41
|
| {
|
|
@@ -42,6 +43,7 @@ export type StepEdit = {
|
|
|
42
43
|
test: number;
|
|
43
44
|
inputs?: TestInput[];
|
|
44
45
|
outputs?: string[];
|
|
46
|
+
annotations?: TestAnnotation[];
|
|
45
47
|
};
|
|
46
48
|
/**
|
|
47
49
|
* Applies one edit and returns the new source. Only the touched statement changes, plus the test's parameters
|
package/dist/steps/edit.js
CHANGED
|
@@ -158,7 +158,7 @@ export function applyEdit(source, edit) {
|
|
|
158
158
|
return withStep(source, parsed, edit.step, (text, code) => insertCode(text, parsed, edit.index, code));
|
|
159
159
|
case "declare": {
|
|
160
160
|
let next = source;
|
|
161
|
-
for (const key of ["inputs", "outputs"]) {
|
|
161
|
+
for (const key of ["inputs", "outputs", "annotations"]) {
|
|
162
162
|
const list = edit[key];
|
|
163
163
|
if (list)
|
|
164
164
|
next = declareList(next, pickTest(next, edit.test), key, list);
|
package/dist/steps/model.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Modifier } from "../types/test.js";
|
|
1
|
+
import type { Modifier, TestAnnotation } from "../types/test.js";
|
|
2
2
|
/**
|
|
3
3
|
* One call of a locator expression, as written in a spec: `page.getByRole("button", { name: "Save" })`.
|
|
4
4
|
* A RegExp text or name is tested as written, so it never comes with `exact`, `match` or `ignoreCase`.
|
|
@@ -264,6 +264,8 @@ export interface ParsedTest {
|
|
|
264
264
|
inputs?: TestInput[];
|
|
265
265
|
/** The `outputs` of the test's options object; undefined without the key */
|
|
266
266
|
outputs?: string[];
|
|
267
|
+
/** The `annotations` of the test's options object; undefined without the key or with an empty list */
|
|
268
|
+
annotations?: TestAnnotation[];
|
|
267
269
|
/** The range of the test's options object (`test(title, { … }, fn)`), when it has one */
|
|
268
270
|
options?: {
|
|
269
271
|
start: number;
|
package/dist/steps/parse.js
CHANGED
|
@@ -137,7 +137,7 @@ export function parseSpecFile(source) {
|
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
const { callback, options = {}, range } = args;
|
|
140
|
-
const { reason, inputs, outputs } = options;
|
|
140
|
+
const { reason, inputs, outputs, annotations } = options;
|
|
141
141
|
const body = callbackBody(callback);
|
|
142
142
|
if (!body) {
|
|
143
143
|
unparsed.push(unreadable(node, kind, "not-inline"));
|
|
@@ -161,6 +161,7 @@ export function parseSpecFile(source) {
|
|
|
161
161
|
...effectiveMark(mark, describes),
|
|
162
162
|
...(inputs && { inputs }),
|
|
163
163
|
...(outputs && { outputs }),
|
|
164
|
+
...(annotations && { annotations }),
|
|
164
165
|
...(range && { options: range }),
|
|
165
166
|
titleEnd: node.arguments[0].end,
|
|
166
167
|
});
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import type { Modifier } from "../types/test.js";
|
|
2
|
+
import type { Modifier, TestAnnotation } from "../types/test.js";
|
|
3
3
|
import type { ParsedTest, TestInput } from "./model.js";
|
|
4
4
|
/** What a `test()` or `describe()` options object says, as far as the codec reads it */
|
|
5
5
|
export interface TestOptionsRead {
|
|
6
6
|
reason?: string;
|
|
7
7
|
inputs?: TestInput[];
|
|
8
8
|
outputs?: string[];
|
|
9
|
+
annotations?: TestAnnotation[];
|
|
9
10
|
}
|
|
10
|
-
type ListKey = "inputs" | "outputs";
|
|
11
|
+
type ListKey = "inputs" | "outputs" | "annotations";
|
|
12
|
+
type ListValue = TestInput[] | string[] | TestAnnotation[];
|
|
11
13
|
/**
|
|
12
14
|
* The options object of a test or describe call: a describe takes only `reason`, a test also `inputs` and
|
|
13
15
|
* `outputs`. Undefined when the object isn't a literal the codec can read, so the call is left unparsed. An
|
|
@@ -21,5 +23,5 @@ export declare function readTestOptions(node: ts.Expression, kind: "test" | "des
|
|
|
21
23
|
* the object, its layout and a `reason`, stays as it is; an unchanged list leaves the source as it is. Throws for
|
|
22
24
|
* names the runtime rejects, before writing
|
|
23
25
|
*/
|
|
24
|
-
export declare function declareList(source: string, parsed: ParsedTest, key: ListKey, list:
|
|
26
|
+
export declare function declareList(source: string, parsed: ParsedTest, key: ListKey, list: ListValue): string;
|
|
25
27
|
export {};
|
|
@@ -33,6 +33,35 @@ function readOutputs(node) {
|
|
|
33
33
|
? names
|
|
34
34
|
: undefined;
|
|
35
35
|
}
|
|
36
|
+
/** `[{ type: "plan", description: "…" }, …]`; undefined for anything but string literals under those two keys */
|
|
37
|
+
function readAnnotations(node) {
|
|
38
|
+
if (!ts.isArrayLiteralExpression(node))
|
|
39
|
+
return undefined;
|
|
40
|
+
const notes = [];
|
|
41
|
+
for (const element of node.elements) {
|
|
42
|
+
if (!ts.isObjectLiteralExpression(element))
|
|
43
|
+
return undefined;
|
|
44
|
+
const note = {};
|
|
45
|
+
for (const property of element.properties) {
|
|
46
|
+
if (!ts.isPropertyAssignment(property))
|
|
47
|
+
return undefined;
|
|
48
|
+
const key = keyOf(property.name);
|
|
49
|
+
const value = stringOf(property.initializer);
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (key === "type")
|
|
53
|
+
note.type = value;
|
|
54
|
+
else if (key === "description")
|
|
55
|
+
note.description = value;
|
|
56
|
+
else
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
if (note.type === undefined || note.type === "")
|
|
60
|
+
return undefined;
|
|
61
|
+
notes.push(note);
|
|
62
|
+
}
|
|
63
|
+
return notes;
|
|
64
|
+
}
|
|
36
65
|
/**
|
|
37
66
|
* The options object of a test or describe call: a describe takes only `reason`, a test also `inputs` and
|
|
38
67
|
* `outputs`. Undefined when the object isn't a literal the codec can read, so the call is left unparsed. An
|
|
@@ -67,13 +96,27 @@ export function readTestOptions(node, kind, modifier) {
|
|
|
67
96
|
if (outputs.length > 0)
|
|
68
97
|
read.outputs = outputs;
|
|
69
98
|
}
|
|
99
|
+
else if (kind === "test" && key === "annotations") {
|
|
100
|
+
const annotations = readAnnotations(value);
|
|
101
|
+
if (!annotations)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (annotations.length > 0)
|
|
104
|
+
read.annotations = annotations;
|
|
105
|
+
}
|
|
70
106
|
else
|
|
71
107
|
return undefined;
|
|
72
108
|
}
|
|
73
109
|
return read;
|
|
74
110
|
}
|
|
75
111
|
const str = (value) => JSON.stringify(value);
|
|
112
|
+
function annotationToSource(note) {
|
|
113
|
+
return note.description === undefined
|
|
114
|
+
? `{ type: ${str(note.type)} }`
|
|
115
|
+
: `{ type: ${str(note.type)}, description: ${str(note.description)} }`;
|
|
116
|
+
}
|
|
76
117
|
function listToSource(key, list) {
|
|
118
|
+
if (key === "annotations")
|
|
119
|
+
return `[${list.map(annotationToSource).join(", ")}]`;
|
|
77
120
|
if (key === "outputs")
|
|
78
121
|
return `[${list.map(str).join(", ")}]`;
|
|
79
122
|
const entries = list.map(({ name, default: value }) => `${IDENTIFIER.test(name) ? name : str(name)}: ${str(value)}`);
|
|
@@ -82,6 +125,12 @@ function listToSource(key, list) {
|
|
|
82
125
|
const sameList = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
83
126
|
/** Throws for a list the runtime's `test()` would reject: a name that isn't valid, a name twice, a default that isn't text */
|
|
84
127
|
function checkList(key, list) {
|
|
128
|
+
if (key === "annotations") {
|
|
129
|
+
for (const note of list)
|
|
130
|
+
if (typeof note.type !== "string" || note.type === "")
|
|
131
|
+
throw new Error(`applyEdit(): an annotation needs a non-empty type`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
85
134
|
const kind = key === "inputs" ? "input" : "output";
|
|
86
135
|
const seen = new Set();
|
|
87
136
|
for (const entry of list) {
|
package/dist/types/config.d.ts
CHANGED
|
@@ -204,6 +204,12 @@ export interface SamuraiTestConfig {
|
|
|
204
204
|
* @default "files"
|
|
205
205
|
*/
|
|
206
206
|
filesDir?: string;
|
|
207
|
+
/**
|
|
208
|
+
* Folder, relative to the project folder, where test plans are kept; `samurai plan "<goal>"` without `--out`
|
|
209
|
+
* writes `<plansDir>/<slug-of-goal>.plan.md`
|
|
210
|
+
* @default "plans"
|
|
211
|
+
*/
|
|
212
|
+
plansDir?: string;
|
|
207
213
|
/**
|
|
208
214
|
* The AI agent (`samurai generate`). `model` is a `provider:name` pair, such as `anthropic:claude-sonnet-4-5`;
|
|
209
215
|
* never a key (keys come from the environment). The `--model` flag and `SAMURAI_AI_MODEL` win over it
|
package/dist/types/test.d.ts
CHANGED
|
@@ -170,6 +170,8 @@ export interface TestOptions extends TestDetails {
|
|
|
170
170
|
inputs?: Record<string, string>;
|
|
171
171
|
/** Names of the text values the test saves for later tests to use */
|
|
172
172
|
outputs?: string[];
|
|
173
|
+
/** Notes on the test, such as `{ type: "plan", description: "plans/pay.plan.md" }`; the report lists them first */
|
|
174
|
+
annotations?: TestAnnotation[];
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
/** Runs before or after each test of a suite, in the test's browser, with the test's fixtures */
|
|
@@ -225,6 +227,8 @@ export interface RegisteredTestCase extends TestCaseBase {
|
|
|
225
227
|
inputs?: Record<string, string>;
|
|
226
228
|
/** The names of the outputs the test declared */
|
|
227
229
|
outputs?: string[];
|
|
230
|
+
/** The annotations the test declared; every run of the test starts with them */
|
|
231
|
+
annotations?: TestAnnotation[];
|
|
228
232
|
/** Registered with `test.fail("title", fn)`: expected to fail; `description` is the `{ reason }` given */
|
|
229
233
|
expectedFail?: { description?: string };
|
|
230
234
|
/** The custom fixtures of the `test` it was written with (`test.extend()`); absent for the plain `test` */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itmetsam/samurai-framework",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.6",
|
|
4
4
|
"description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
|
|
5
5
|
"main": "dist/api.js",
|
|
6
6
|
"exports": {
|