@itmetsam/samurai-framework 0.7.0 → 0.7.1
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/cli/main.js +2 -2
- package/dist/recorder/session.d.ts +2 -0
- package/dist/recorder/session.js +5 -1
- package/dist/steps/edit.d.ts +3 -2
- package/dist/steps/edit.js +28 -4
- package/dist/steps/model.d.ts +10 -0
- package/dist/steps/parse.js +23 -0
- package/package.json +1 -1
package/dist/cli/main.js
CHANGED
|
@@ -145,8 +145,8 @@ export async function main(argv, io) {
|
|
|
145
145
|
});
|
|
146
146
|
if (!values.json) {
|
|
147
147
|
io.out(`\nRecorded ${count} ${count === 1 ? "step" : "steps"} into "${result.test}" in ${result.file}\n`);
|
|
148
|
-
if (result.
|
|
149
|
-
io.out(`Set ${result.
|
|
148
|
+
if (result.unsetSecrets.length > 0) {
|
|
149
|
+
io.out(`Set ${result.unsetSecrets.map((name) => `SAMURAI_SECRET_${name}`).join(", ")} (environment variable or .env.<environment>) before running it.\n`);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
return 0;
|
|
@@ -48,6 +48,8 @@ export interface RecordResult {
|
|
|
48
48
|
events: RecorderEvent[];
|
|
49
49
|
/** Names the recorded steps read with `secrets.<NAME>`: they must be set before the test can run */
|
|
50
50
|
secrets: string[];
|
|
51
|
+
/** Those of `secrets` that have no value in the recording's environment yet, read when the recording ended */
|
|
52
|
+
unsetSecrets: string[];
|
|
51
53
|
}
|
|
52
54
|
/** What the toolbar shows of the project: its environment, variables, secrets and the test's screenshots */
|
|
53
55
|
export declare function toolbarOptions(run: Pick<PreparedRun, "settings" | "secrets">, source: string, dir: string, config: Pick<SamuraiTestConfig, "environments">, testIndex: number): Promise<ControllerOptions>;
|
package/dist/recorder/session.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { Browser } from "../browser/browser.js";
|
|
4
4
|
import { ensureWarm } from "../browser/warm-up.js";
|
|
5
5
|
import { loadConfig, projectDir } from "../config/config.js";
|
|
6
|
+
import { loadSecrets } from "../config/secrets.js";
|
|
6
7
|
import { isGitIgnored } from "../project/index.js";
|
|
7
8
|
import { withProject } from "../runner/project.js";
|
|
8
9
|
import { parseSpec } from "../steps/parse.js";
|
|
@@ -165,6 +166,9 @@ export async function recordSpec(options) {
|
|
|
165
166
|
await browser.close().catch(() => undefined);
|
|
166
167
|
}
|
|
167
168
|
const secrets = resultSecrets(events, addedSecrets);
|
|
168
|
-
|
|
169
|
+
// Read again: the toolbar may have written new ones to the .env file
|
|
170
|
+
const set = loadSecrets(settings.environment);
|
|
171
|
+
const unsetSecrets = secrets.filter((name) => !set.has(name));
|
|
172
|
+
return { file, test: test.name, events, secrets, unsetSecrets };
|
|
169
173
|
});
|
|
170
174
|
}
|
package/dist/steps/edit.d.ts
CHANGED
|
@@ -24,7 +24,8 @@ export type StepEdit = {
|
|
|
24
24
|
to: number;
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
27
|
-
* Applies one edit and returns the new source. Only the touched statement changes
|
|
28
|
-
*
|
|
27
|
+
* Applies one edit and returns the new source. Only the touched statement changes, plus the test's parameters
|
|
28
|
+
* when the step reads a fixture (`env`, `secrets`) they don't have yet: everything else, formatting and
|
|
29
|
+
* comments included, stays byte for byte.
|
|
29
30
|
*/
|
|
30
31
|
export declare function applyEdit(source: string, edit: StepEdit): string;
|
package/dist/steps/edit.js
CHANGED
|
@@ -51,19 +51,43 @@ function removeStep(source, parsed, index) {
|
|
|
51
51
|
const { anchor, end } = pickStep(parsed, index);
|
|
52
52
|
return splice(source, lineStart(source, anchor), lineEnd(source, end), "");
|
|
53
53
|
}
|
|
54
|
+
/** The fixtures a step's statement reads */
|
|
55
|
+
function fixturesOf(step) {
|
|
56
|
+
if (step.kind !== "fill" || step.value.kind === "literal")
|
|
57
|
+
return [];
|
|
58
|
+
return [step.value.kind === "env" ? "env" : "secrets"];
|
|
59
|
+
}
|
|
60
|
+
/** Adds the fixtures the step reads to the test's parameters, when they aren't there yet */
|
|
61
|
+
function withFixtures(source, parsed, step) {
|
|
62
|
+
const { fixtures } = parsed;
|
|
63
|
+
if (!fixtures)
|
|
64
|
+
return source;
|
|
65
|
+
const missing = fixturesOf(step).filter((name) => !fixtures.names.includes(name));
|
|
66
|
+
if (missing.length === 0)
|
|
67
|
+
return source;
|
|
68
|
+
const list = missing.join(", ");
|
|
69
|
+
const text = fixtures.shape === "list"
|
|
70
|
+
? `, ${list}`
|
|
71
|
+
: fixtures.shape === "empty"
|
|
72
|
+
? ` ${list} `
|
|
73
|
+
: `{ ${list} }`;
|
|
74
|
+
return splice(source, fixtures.at, fixtures.at, text);
|
|
75
|
+
}
|
|
54
76
|
/**
|
|
55
|
-
* Applies one edit and returns the new source. Only the touched statement changes
|
|
56
|
-
*
|
|
77
|
+
* Applies one edit and returns the new source. Only the touched statement changes, plus the test's parameters
|
|
78
|
+
* when the step reads a fixture (`env`, `secrets`) they don't have yet: everything else, formatting and
|
|
79
|
+
* comments included, stays byte for byte.
|
|
57
80
|
*/
|
|
58
81
|
export function applyEdit(source, edit) {
|
|
59
82
|
const parsed = pickTest(source, edit.test);
|
|
60
83
|
switch (edit.op) {
|
|
61
84
|
case "replace": {
|
|
62
85
|
const { start, end } = pickStep(parsed, edit.index);
|
|
63
|
-
|
|
86
|
+
// The parameters come before the body: adding to them leaves the step's offsets valid
|
|
87
|
+
return withFixtures(splice(source, start, end, stepToSource(edit.step)), parsed, edit.step);
|
|
64
88
|
}
|
|
65
89
|
case "insert":
|
|
66
|
-
return insertCode(source, parsed, edit.index, stepToSource(edit.step));
|
|
90
|
+
return withFixtures(insertCode(source, parsed, edit.index, stepToSource(edit.step)), parsed, edit.step);
|
|
67
91
|
case "remove":
|
|
68
92
|
return removeStep(source, parsed, edit.index);
|
|
69
93
|
case "move": {
|
package/dist/steps/model.d.ts
CHANGED
|
@@ -117,4 +117,14 @@ export interface ParsedTest {
|
|
|
117
117
|
/** Offset just after the `{` of the test body, and of its closing `}` */
|
|
118
118
|
bodyStart: number;
|
|
119
119
|
bodyEnd: number;
|
|
120
|
+
/**
|
|
121
|
+
* The fixtures the test's callback destructures, and the offset where another one goes: after the last one
|
|
122
|
+
* (`list`), inside an empty `{}` (`empty`) or inside the empty `()` of a callback without parameters (`none`).
|
|
123
|
+
* Undefined when the parameter can't take one: a plain identifier, or a rest element
|
|
124
|
+
*/
|
|
125
|
+
fixtures?: {
|
|
126
|
+
names: string[];
|
|
127
|
+
at: number;
|
|
128
|
+
shape: "list" | "empty" | "none";
|
|
129
|
+
};
|
|
120
130
|
}
|
package/dist/steps/parse.js
CHANGED
|
@@ -257,6 +257,28 @@ function parseStatement(source, statement, sourceFile) {
|
|
|
257
257
|
anchor: comments?.[0]?.pos ?? start,
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
|
+
function callbackFixtures(callback, sourceFile) {
|
|
261
|
+
const [first] = callback.parameters;
|
|
262
|
+
if (!first) {
|
|
263
|
+
const open = callback
|
|
264
|
+
.getChildren(sourceFile)
|
|
265
|
+
.find((child) => child.kind === ts.SyntaxKind.OpenParenToken);
|
|
266
|
+
return open ? { names: [], at: open.end, shape: "none" } : undefined;
|
|
267
|
+
}
|
|
268
|
+
const pattern = first.name;
|
|
269
|
+
if (!ts.isObjectBindingPattern(pattern))
|
|
270
|
+
return undefined;
|
|
271
|
+
if (pattern.elements.some((element) => element.dotDotDotToken))
|
|
272
|
+
return undefined;
|
|
273
|
+
const last = pattern.elements[pattern.elements.length - 1];
|
|
274
|
+
if (!last)
|
|
275
|
+
return { names: [], at: pattern.getStart(sourceFile) + 1, shape: "empty" };
|
|
276
|
+
return {
|
|
277
|
+
names: pattern.elements.map(({ propertyName, name }) => (propertyName ?? name).getText(sourceFile)),
|
|
278
|
+
at: last.end,
|
|
279
|
+
shape: "list",
|
|
280
|
+
};
|
|
281
|
+
}
|
|
260
282
|
function callbackBody(node) {
|
|
261
283
|
return node &&
|
|
262
284
|
(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
|
|
@@ -280,6 +302,7 @@ export function parseSpec(source) {
|
|
|
280
302
|
steps: body.statements.map((statement) => parseStatement(source, statement, sourceFile)),
|
|
281
303
|
bodyStart: body.getStart(sourceFile) + 1,
|
|
282
304
|
bodyEnd: body.end - 1,
|
|
305
|
+
fixtures: callbackFixtures(node.arguments[1], sourceFile),
|
|
283
306
|
});
|
|
284
307
|
return;
|
|
285
308
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itmetsam/samurai-framework",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
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": {
|