@testsmith/testblocks 0.6.0 → 0.8.0
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/executor.d.ts +4 -1
- package/dist/cli/executor.js +101 -5
- package/dist/cli/index.js +148 -4
- package/dist/cli/reporters/ConsoleReporter.d.ts +12 -0
- package/dist/cli/reporters/ConsoleReporter.js +39 -0
- package/dist/cli/reporters/HTMLReporter.d.ts +19 -0
- package/dist/cli/reporters/HTMLReporter.js +506 -0
- package/dist/cli/reporters/JSONReporter.d.ts +15 -0
- package/dist/cli/reporters/JSONReporter.js +80 -0
- package/dist/cli/reporters/JUnitReporter.d.ts +19 -0
- package/dist/cli/reporters/JUnitReporter.js +105 -0
- package/dist/cli/reporters/index.d.ts +17 -0
- package/dist/cli/reporters/index.js +31 -0
- package/dist/cli/reporters/types.d.ts +28 -0
- package/dist/cli/reporters/types.js +2 -0
- package/dist/cli/reporters/utils.d.ts +31 -0
- package/dist/cli/reporters/utils.js +136 -0
- package/dist/cli/reporters.d.ts +13 -62
- package/dist/cli/reporters.js +16 -719
- package/dist/client/assets/index-Boo8ZrY_.js +2195 -0
- package/dist/client/assets/{index-dXniUrbi.js.map → index-Boo8ZrY_.js.map} +1 -1
- package/dist/client/assets/index-OxNH9dW-.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/core/blocks/api.js +3 -6
- package/dist/core/blocks/assertions.d.ts +31 -0
- package/dist/core/blocks/assertions.js +72 -0
- package/dist/core/blocks/index.d.ts +1 -0
- package/dist/core/blocks/index.js +6 -1
- package/dist/core/blocks/lifecycle.js +5 -3
- package/dist/core/blocks/logic.js +2 -3
- package/dist/core/blocks/playwright/assertions.d.ts +5 -0
- package/dist/core/blocks/playwright/assertions.js +321 -0
- package/dist/core/blocks/playwright/index.d.ts +17 -0
- package/dist/core/blocks/playwright/index.js +49 -0
- package/dist/core/blocks/playwright/interactions.d.ts +5 -0
- package/dist/core/blocks/playwright/interactions.js +191 -0
- package/dist/core/blocks/playwright/navigation.d.ts +5 -0
- package/dist/core/blocks/playwright/navigation.js +133 -0
- package/dist/core/blocks/playwright/retrieval.d.ts +5 -0
- package/dist/core/blocks/playwright/retrieval.js +144 -0
- package/dist/core/blocks/playwright/types.d.ts +65 -0
- package/dist/core/blocks/playwright/types.js +5 -0
- package/dist/core/blocks/playwright/utils.d.ts +26 -0
- package/dist/core/blocks/playwright/utils.js +137 -0
- package/dist/core/blocks/playwright.d.ts +13 -2
- package/dist/core/blocks/playwright.js +14 -761
- package/dist/core/executor/BaseTestExecutor.d.ts +60 -0
- package/dist/core/executor/BaseTestExecutor.js +297 -0
- package/dist/core/executor/index.d.ts +1 -0
- package/dist/core/executor/index.js +5 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/types.d.ts +12 -0
- package/dist/core/utils/blocklyParser.d.ts +18 -0
- package/dist/core/utils/blocklyParser.js +84 -0
- package/dist/core/utils/dataLoader.d.ts +9 -0
- package/dist/core/utils/dataLoader.js +117 -0
- package/dist/core/utils/index.d.ts +2 -0
- package/dist/core/utils/index.js +12 -0
- package/dist/core/utils/logger.d.ts +14 -0
- package/dist/core/utils/logger.js +48 -0
- package/dist/core/utils/variableResolver.d.ts +24 -0
- package/dist/core/utils/variableResolver.js +92 -0
- package/dist/server/executor.d.ts +6 -0
- package/dist/server/executor.js +207 -47
- package/dist/server/globals.d.ts +6 -1
- package/dist/server/globals.js +7 -0
- package/dist/server/startServer.js +15 -0
- package/package.json +1 -1
- package/dist/client/assets/index-dXniUrbi.js +0 -2193
- package/dist/client/assets/index-oTTttNKd.css +0 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Logger } from '../types';
|
|
2
|
+
export interface LoggerOptions {
|
|
3
|
+
prefix?: string;
|
|
4
|
+
indent?: string;
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Create a logger instance for test execution
|
|
9
|
+
*/
|
|
10
|
+
export declare function createLogger(options?: LoggerOptions): Logger;
|
|
11
|
+
/**
|
|
12
|
+
* Create a CLI-style logger with simpler output
|
|
13
|
+
*/
|
|
14
|
+
export declare function createCliLogger(): Logger;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLogger = createLogger;
|
|
4
|
+
exports.createCliLogger = createCliLogger;
|
|
5
|
+
/**
|
|
6
|
+
* Create a logger instance for test execution
|
|
7
|
+
*/
|
|
8
|
+
function createLogger(options = {}) {
|
|
9
|
+
const { prefix = '', indent = ' ', debug = false } = options;
|
|
10
|
+
const logPrefix = prefix ? `${prefix} ` : '';
|
|
11
|
+
return {
|
|
12
|
+
info: (message, data) => {
|
|
13
|
+
console.log(`${indent}[INFO] ${logPrefix}${message}`, data !== undefined ? data : '');
|
|
14
|
+
},
|
|
15
|
+
warn: (message, data) => {
|
|
16
|
+
console.warn(`${indent}[WARN] ${logPrefix}${message}`, data !== undefined ? data : '');
|
|
17
|
+
},
|
|
18
|
+
error: (message, data) => {
|
|
19
|
+
console.error(`${indent}[ERROR] ${logPrefix}${message}`, data !== undefined ? data : '');
|
|
20
|
+
},
|
|
21
|
+
debug: (message, data) => {
|
|
22
|
+
if (debug || process.env.DEBUG) {
|
|
23
|
+
console.debug(`${indent}[DEBUG] ${logPrefix}${message}`, data !== undefined ? data : '');
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a CLI-style logger with simpler output
|
|
30
|
+
*/
|
|
31
|
+
function createCliLogger() {
|
|
32
|
+
return {
|
|
33
|
+
info: (message, data) => {
|
|
34
|
+
console.log(` ${message}`, data !== undefined ? data : '');
|
|
35
|
+
},
|
|
36
|
+
warn: (message, data) => {
|
|
37
|
+
console.warn(` \u26a0 ${message}`, data !== undefined ? data : '');
|
|
38
|
+
},
|
|
39
|
+
error: (message, data) => {
|
|
40
|
+
console.error(` \u2717 ${message}`, data !== undefined ? data : '');
|
|
41
|
+
},
|
|
42
|
+
debug: (message, data) => {
|
|
43
|
+
if (process.env.DEBUG) {
|
|
44
|
+
console.debug(` [debug] ${message}`, data !== undefined ? data : '');
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ExecutionContext } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Utility class for resolving variable placeholders in strings
|
|
4
|
+
*/
|
|
5
|
+
export declare class VariableResolver {
|
|
6
|
+
/**
|
|
7
|
+
* Resolve ${variable} placeholders in a string using context variables and currentData
|
|
8
|
+
* Supports dot notation for nested object access (e.g., ${user.email})
|
|
9
|
+
*/
|
|
10
|
+
static resolve(text: string, context: ExecutionContext): string;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve variables in an object recursively
|
|
13
|
+
*/
|
|
14
|
+
static resolveObject(obj: unknown, context: ExecutionContext): unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Check if a string contains variable placeholders
|
|
17
|
+
*/
|
|
18
|
+
static hasVariables(text: string): boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve variable defaults from globals format
|
|
22
|
+
* Handles objects with { type, default, description } structure
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveVariableDefaults(vars?: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VariableResolver = void 0;
|
|
4
|
+
exports.resolveVariableDefaults = resolveVariableDefaults;
|
|
5
|
+
/**
|
|
6
|
+
* Utility class for resolving variable placeholders in strings
|
|
7
|
+
*/
|
|
8
|
+
class VariableResolver {
|
|
9
|
+
/**
|
|
10
|
+
* Resolve ${variable} placeholders in a string using context variables and currentData
|
|
11
|
+
* Supports dot notation for nested object access (e.g., ${user.email})
|
|
12
|
+
*/
|
|
13
|
+
static resolve(text, context) {
|
|
14
|
+
if (typeof text !== 'string')
|
|
15
|
+
return text;
|
|
16
|
+
return text.replace(/\$\{([\w.]+)\}/g, (match, path) => {
|
|
17
|
+
const parts = path.split('.');
|
|
18
|
+
const varName = parts[0];
|
|
19
|
+
// Check currentData first (for data-driven tests)
|
|
20
|
+
if (context.currentData?.values[varName] !== undefined) {
|
|
21
|
+
let value = context.currentData.values[varName];
|
|
22
|
+
// Handle dot notation for nested access
|
|
23
|
+
if (parts.length > 1 && value !== null && typeof value === 'object') {
|
|
24
|
+
for (let i = 1; i < parts.length; i++) {
|
|
25
|
+
if (value === undefined || value === null)
|
|
26
|
+
break;
|
|
27
|
+
value = value[parts[i]];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (value !== undefined && value !== null) {
|
|
31
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Then check context variables
|
|
35
|
+
let value = context.variables.get(varName);
|
|
36
|
+
if (parts.length > 1 && value !== undefined && value !== null) {
|
|
37
|
+
for (let i = 1; i < parts.length; i++) {
|
|
38
|
+
if (value === undefined || value === null)
|
|
39
|
+
break;
|
|
40
|
+
value = value[parts[i]];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (value === undefined || value === null)
|
|
44
|
+
return match;
|
|
45
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve variables in an object recursively
|
|
50
|
+
*/
|
|
51
|
+
static resolveObject(obj, context) {
|
|
52
|
+
if (typeof obj === 'string') {
|
|
53
|
+
return this.resolve(obj, context);
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(obj)) {
|
|
56
|
+
return obj.map(item => this.resolveObject(item, context));
|
|
57
|
+
}
|
|
58
|
+
if (obj && typeof obj === 'object') {
|
|
59
|
+
const result = {};
|
|
60
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
61
|
+
result[key] = this.resolveObject(value, context);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
return obj;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if a string contains variable placeholders
|
|
69
|
+
*/
|
|
70
|
+
static hasVariables(text) {
|
|
71
|
+
return typeof text === 'string' && /\$\{[\w.]+\}/.test(text);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.VariableResolver = VariableResolver;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve variable defaults from globals format
|
|
77
|
+
* Handles objects with { type, default, description } structure
|
|
78
|
+
*/
|
|
79
|
+
function resolveVariableDefaults(vars) {
|
|
80
|
+
if (!vars)
|
|
81
|
+
return {};
|
|
82
|
+
const resolved = {};
|
|
83
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
84
|
+
if (value && typeof value === 'object' && 'default' in value) {
|
|
85
|
+
resolved[key] = value.default;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
resolved[key] = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return resolved;
|
|
92
|
+
}
|
|
@@ -7,6 +7,7 @@ export interface ExecutorOptions {
|
|
|
7
7
|
plugins?: Plugin[];
|
|
8
8
|
testIdAttribute?: string;
|
|
9
9
|
baseDir?: string;
|
|
10
|
+
procedures?: Record<string, ProcedureDefinition>;
|
|
10
11
|
}
|
|
11
12
|
export declare class TestExecutor {
|
|
12
13
|
private options;
|
|
@@ -14,6 +15,7 @@ export declare class TestExecutor {
|
|
|
14
15
|
private context;
|
|
15
16
|
private page;
|
|
16
17
|
private plugins;
|
|
18
|
+
private projectProcedures;
|
|
17
19
|
constructor(options?: ExecutorOptions);
|
|
18
20
|
initialize(): Promise<void>;
|
|
19
21
|
cleanup(): Promise<void>;
|
|
@@ -34,5 +36,9 @@ export declare class TestExecutor {
|
|
|
34
36
|
private resolveParams;
|
|
35
37
|
private loadDataFromFile;
|
|
36
38
|
private resolveVariableDefaults;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve ${variable} placeholders in a string using context variables and currentData
|
|
41
|
+
*/
|
|
42
|
+
private static resolveVariablePlaceholders;
|
|
37
43
|
private createLogger;
|
|
38
44
|
}
|
package/dist/server/executor.js
CHANGED
|
@@ -93,6 +93,7 @@ class TestExecutor {
|
|
|
93
93
|
this.context = null;
|
|
94
94
|
this.page = null;
|
|
95
95
|
this.plugins = new Map();
|
|
96
|
+
this.projectProcedures = new Map();
|
|
96
97
|
this.options = {
|
|
97
98
|
headless: true,
|
|
98
99
|
timeout: 30000,
|
|
@@ -104,6 +105,13 @@ class TestExecutor {
|
|
|
104
105
|
this.plugins.set(plugin.name, plugin);
|
|
105
106
|
});
|
|
106
107
|
}
|
|
108
|
+
// Register project-level procedures from options
|
|
109
|
+
if (options.procedures) {
|
|
110
|
+
for (const [name, procedure] of Object.entries(options.procedures)) {
|
|
111
|
+
this.projectProcedures.set(name, procedure);
|
|
112
|
+
}
|
|
113
|
+
this.registerCustomBlocksFromProcedures(options.procedures);
|
|
114
|
+
}
|
|
107
115
|
}
|
|
108
116
|
async initialize() {
|
|
109
117
|
// Set the test ID attribute globally for Playwright selectors
|
|
@@ -169,10 +177,17 @@ class TestExecutor {
|
|
|
169
177
|
await this.initialize();
|
|
170
178
|
}
|
|
171
179
|
// Create shared execution context for lifecycle hooks
|
|
180
|
+
// Merge project-level and file-level procedures (file takes precedence)
|
|
181
|
+
const mergedProcedures = new Map(this.projectProcedures);
|
|
182
|
+
if (testFile.procedures) {
|
|
183
|
+
for (const [name, proc] of Object.entries(testFile.procedures)) {
|
|
184
|
+
mergedProcedures.set(name, proc);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
172
187
|
const sharedContext = {
|
|
173
188
|
variables: new Map(Object.entries({
|
|
174
189
|
...this.resolveVariableDefaults(testFile.variables),
|
|
175
|
-
...this.options.variables,
|
|
190
|
+
...this.resolveVariableDefaults(this.options.variables),
|
|
176
191
|
})),
|
|
177
192
|
results: [],
|
|
178
193
|
browser: this.browser,
|
|
@@ -180,7 +195,9 @@ class TestExecutor {
|
|
|
180
195
|
logger: this.createLogger(),
|
|
181
196
|
plugins: this.plugins,
|
|
182
197
|
testIdAttribute: this.options.testIdAttribute,
|
|
198
|
+
procedures: mergedProcedures,
|
|
183
199
|
};
|
|
200
|
+
let beforeAllFailed = false;
|
|
184
201
|
try {
|
|
185
202
|
// Run beforeAll hooks
|
|
186
203
|
if (testFile.beforeAll && testFile.beforeAll.length > 0) {
|
|
@@ -188,29 +205,49 @@ class TestExecutor {
|
|
|
188
205
|
const beforeAllResult = await this.runLifecycleSteps(testFile.beforeAll, 'beforeAll', sharedContext);
|
|
189
206
|
results.push(beforeAllResult);
|
|
190
207
|
if (beforeAllResult.status === 'failed' || beforeAllResult.status === 'error') {
|
|
191
|
-
// Don't run tests if beforeAll failed
|
|
192
|
-
|
|
208
|
+
// Don't run tests if beforeAll failed, but still run afterAll
|
|
209
|
+
beforeAllFailed = true;
|
|
193
210
|
}
|
|
194
211
|
}
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
testData =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
212
|
+
// Only run tests if beforeAll succeeded
|
|
213
|
+
if (!beforeAllFailed) {
|
|
214
|
+
// Run each test with beforeEach/afterEach
|
|
215
|
+
for (const test of testFile.tests) {
|
|
216
|
+
// Load data from file if specified
|
|
217
|
+
let testData = test.data;
|
|
218
|
+
if (test.dataFile && !testData) {
|
|
219
|
+
testData = this.loadDataFromFile(test.dataFile);
|
|
220
|
+
}
|
|
221
|
+
// Check if test has data-driven sets
|
|
222
|
+
if (testData && testData.length > 0) {
|
|
223
|
+
// Run test for each data set
|
|
224
|
+
for (let i = 0; i < testData.length; i++) {
|
|
225
|
+
const dataSet = testData[i];
|
|
226
|
+
// Run suite-level beforeEach
|
|
227
|
+
if (testFile.beforeEach && testFile.beforeEach.length > 0) {
|
|
228
|
+
for (const step of testFile.beforeEach) {
|
|
229
|
+
await this.runStep(step, sharedContext);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const result = await this.runTestWithData(test, testFile.variables, dataSet, i, sharedContext);
|
|
233
|
+
results.push(result);
|
|
234
|
+
// Run suite-level afterEach
|
|
235
|
+
if (testFile.afterEach && testFile.afterEach.length > 0) {
|
|
236
|
+
for (const step of testFile.afterEach) {
|
|
237
|
+
await this.runStep(step, sharedContext);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Run test once without data
|
|
207
244
|
// Run suite-level beforeEach
|
|
208
245
|
if (testFile.beforeEach && testFile.beforeEach.length > 0) {
|
|
209
246
|
for (const step of testFile.beforeEach) {
|
|
210
247
|
await this.runStep(step, sharedContext);
|
|
211
248
|
}
|
|
212
249
|
}
|
|
213
|
-
const result = await this.
|
|
250
|
+
const result = await this.runTest(test, testFile.variables, sharedContext);
|
|
214
251
|
results.push(result);
|
|
215
252
|
// Run suite-level afterEach
|
|
216
253
|
if (testFile.afterEach && testFile.afterEach.length > 0) {
|
|
@@ -220,32 +257,37 @@ class TestExecutor {
|
|
|
220
257
|
}
|
|
221
258
|
}
|
|
222
259
|
}
|
|
223
|
-
else {
|
|
224
|
-
// Run test once without data
|
|
225
|
-
// Run suite-level beforeEach
|
|
226
|
-
if (testFile.beforeEach && testFile.beforeEach.length > 0) {
|
|
227
|
-
for (const step of testFile.beforeEach) {
|
|
228
|
-
await this.runStep(step, sharedContext);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
const result = await this.runTest(test, testFile.variables, sharedContext);
|
|
232
|
-
results.push(result);
|
|
233
|
-
// Run suite-level afterEach
|
|
234
|
-
if (testFile.afterEach && testFile.afterEach.length > 0) {
|
|
235
|
-
for (const step of testFile.afterEach) {
|
|
236
|
-
await this.runStep(step, sharedContext);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
260
|
}
|
|
241
|
-
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
// Always run afterAll hooks, even if beforeAll failed
|
|
264
|
+
// This ensures cleanup happens regardless of setup failures
|
|
242
265
|
if (testFile.afterAll && testFile.afterAll.length > 0) {
|
|
243
266
|
sharedContext.logger.info('Running afterAll hooks...');
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
try {
|
|
268
|
+
const afterAllResult = await this.runLifecycleSteps(testFile.afterAll, 'afterAll', sharedContext);
|
|
269
|
+
results.push(afterAllResult);
|
|
270
|
+
}
|
|
271
|
+
catch (afterAllError) {
|
|
272
|
+
// Log but don't throw - we still want cleanup to complete
|
|
273
|
+
sharedContext.logger.error('afterAll hook failed', afterAllError.message);
|
|
274
|
+
results.push({
|
|
275
|
+
testId: 'lifecycle-afterAll',
|
|
276
|
+
testName: 'afterAll',
|
|
277
|
+
status: 'error',
|
|
278
|
+
duration: 0,
|
|
279
|
+
steps: [],
|
|
280
|
+
error: {
|
|
281
|
+
message: afterAllError.message,
|
|
282
|
+
stack: afterAllError.stack,
|
|
283
|
+
},
|
|
284
|
+
startedAt: new Date().toISOString(),
|
|
285
|
+
finishedAt: new Date().toISOString(),
|
|
286
|
+
isLifecycle: true,
|
|
287
|
+
lifecycleType: 'afterAll',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
246
290
|
}
|
|
247
|
-
}
|
|
248
|
-
finally {
|
|
249
291
|
await this.cleanup();
|
|
250
292
|
}
|
|
251
293
|
return results;
|
|
@@ -285,7 +327,9 @@ class TestExecutor {
|
|
|
285
327
|
this.registerCustomBlocksFromProcedures(procedures);
|
|
286
328
|
}
|
|
287
329
|
registerCustomBlocksFromProcedures(procedures) {
|
|
288
|
-
Object.
|
|
330
|
+
Object.entries(procedures).forEach(([name, proc]) => {
|
|
331
|
+
// Register in the procedure registry so getProcedure() can find it
|
|
332
|
+
(0, core_1.registerProcedure)(name, proc);
|
|
289
333
|
if (!proc.steps || proc.steps.length === 0)
|
|
290
334
|
return;
|
|
291
335
|
const blockType = `custom_${proc.name.toLowerCase().replace(/\s+/g, '_')}`;
|
|
@@ -308,10 +352,20 @@ class TestExecutor {
|
|
|
308
352
|
execute: async (params, context) => {
|
|
309
353
|
context.logger.info(`Executing custom block: ${proc.name}`);
|
|
310
354
|
// Set procedure parameters in context.variables so ${paramName} references work
|
|
355
|
+
// Resolve any ${variable} placeholders in the parameter values first
|
|
311
356
|
(proc.params || []).forEach(p => {
|
|
312
|
-
const
|
|
357
|
+
const paramKey = p.name.toUpperCase();
|
|
358
|
+
let value = params[paramKey];
|
|
313
359
|
if (value !== undefined) {
|
|
360
|
+
// Resolve variable placeholders like ${email} from context/currentData
|
|
361
|
+
if (typeof value === 'string') {
|
|
362
|
+
value = TestExecutor.resolveVariablePlaceholders(value, context);
|
|
363
|
+
}
|
|
314
364
|
context.variables.set(p.name, value);
|
|
365
|
+
context.logger.debug(`Set procedure param: ${p.name} = "${value}"`);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
context.logger.warn(`Procedure param not found in params: ${paramKey} (available: ${Object.keys(params).join(', ')})`);
|
|
315
369
|
}
|
|
316
370
|
});
|
|
317
371
|
return {
|
|
@@ -333,7 +387,7 @@ class TestExecutor {
|
|
|
333
387
|
? Object.fromEntries(sharedContext.variables)
|
|
334
388
|
: {
|
|
335
389
|
...this.resolveVariableDefaults(fileVariables),
|
|
336
|
-
...this.options.variables,
|
|
390
|
+
...this.resolveVariableDefaults(this.options.variables),
|
|
337
391
|
};
|
|
338
392
|
const context = {
|
|
339
393
|
variables: new Map(Object.entries(baseVariables)),
|
|
@@ -343,6 +397,11 @@ class TestExecutor {
|
|
|
343
397
|
logger: this.createLogger(),
|
|
344
398
|
plugins: this.plugins,
|
|
345
399
|
testIdAttribute: this.options.testIdAttribute,
|
|
400
|
+
// Inherit procedures from shared context
|
|
401
|
+
procedures: sharedContext?.procedures || new Map(),
|
|
402
|
+
// Enable soft assertions if configured on the test
|
|
403
|
+
softAssertions: test.softAssertions || false,
|
|
404
|
+
softAssertionErrors: [],
|
|
346
405
|
};
|
|
347
406
|
// Run beforeTest hooks
|
|
348
407
|
for (const plugin of this.plugins.values()) {
|
|
@@ -352,6 +411,7 @@ class TestExecutor {
|
|
|
352
411
|
}
|
|
353
412
|
let testStatus = 'passed';
|
|
354
413
|
let testError;
|
|
414
|
+
let collectedSoftErrors = [];
|
|
355
415
|
try {
|
|
356
416
|
// Execute steps from Blockly serialization format
|
|
357
417
|
const steps = this.extractStepsFromBlocklyState(test.steps);
|
|
@@ -361,9 +421,21 @@ class TestExecutor {
|
|
|
361
421
|
if (stepResult.status === 'failed' || stepResult.status === 'error') {
|
|
362
422
|
testStatus = stepResult.status;
|
|
363
423
|
testError = stepResult.error;
|
|
364
|
-
|
|
424
|
+
// In soft assertion mode, continue executing remaining steps
|
|
425
|
+
if (!context.softAssertions) {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
365
428
|
}
|
|
366
429
|
}
|
|
430
|
+
// Check for soft assertion errors at the end of the test
|
|
431
|
+
collectedSoftErrors = context.softAssertionErrors || [];
|
|
432
|
+
if (collectedSoftErrors.length > 0 && testStatus === 'passed') {
|
|
433
|
+
testStatus = 'failed';
|
|
434
|
+
const errorMessages = collectedSoftErrors.map((e, i) => ` ${i + 1}. ${e.message}`).join('\n');
|
|
435
|
+
testError = {
|
|
436
|
+
message: `${collectedSoftErrors.length} soft assertion(s) failed:\n${errorMessages}`,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
367
439
|
}
|
|
368
440
|
catch (error) {
|
|
369
441
|
testStatus = 'error';
|
|
@@ -381,6 +453,7 @@ class TestExecutor {
|
|
|
381
453
|
error: testError,
|
|
382
454
|
startedAt,
|
|
383
455
|
finishedAt: new Date().toISOString(),
|
|
456
|
+
softAssertionErrors: collectedSoftErrors.length > 0 ? collectedSoftErrors : undefined,
|
|
384
457
|
};
|
|
385
458
|
// Run afterTest hooks
|
|
386
459
|
for (const plugin of this.plugins.values()) {
|
|
@@ -403,7 +476,7 @@ class TestExecutor {
|
|
|
403
476
|
? Object.fromEntries(sharedContext.variables)
|
|
404
477
|
: {
|
|
405
478
|
...this.resolveVariableDefaults(fileVariables),
|
|
406
|
-
...this.options.variables,
|
|
479
|
+
...this.resolveVariableDefaults(this.options.variables),
|
|
407
480
|
};
|
|
408
481
|
const context = {
|
|
409
482
|
variables: new Map(Object.entries(baseVariables)),
|
|
@@ -413,8 +486,13 @@ class TestExecutor {
|
|
|
413
486
|
logger: this.createLogger(),
|
|
414
487
|
plugins: this.plugins,
|
|
415
488
|
testIdAttribute: this.options.testIdAttribute,
|
|
489
|
+
// Inherit procedures from shared context
|
|
490
|
+
procedures: sharedContext?.procedures || new Map(),
|
|
416
491
|
currentData: dataSet,
|
|
417
492
|
dataIndex,
|
|
493
|
+
// Enable soft assertions if configured on the test
|
|
494
|
+
softAssertions: test.softAssertions || false,
|
|
495
|
+
softAssertionErrors: [],
|
|
418
496
|
};
|
|
419
497
|
// Inject data values into variables
|
|
420
498
|
for (const [key, value] of Object.entries(dataSet.values)) {
|
|
@@ -428,6 +506,7 @@ class TestExecutor {
|
|
|
428
506
|
}
|
|
429
507
|
let testStatus = 'passed';
|
|
430
508
|
let testError;
|
|
509
|
+
let collectedSoftErrors = [];
|
|
431
510
|
try {
|
|
432
511
|
// Execute steps from Blockly serialization format
|
|
433
512
|
const steps = this.extractStepsFromBlocklyState(test.steps);
|
|
@@ -437,9 +516,21 @@ class TestExecutor {
|
|
|
437
516
|
if (stepResult.status === 'failed' || stepResult.status === 'error') {
|
|
438
517
|
testStatus = stepResult.status;
|
|
439
518
|
testError = stepResult.error;
|
|
440
|
-
|
|
519
|
+
// In soft assertion mode, continue executing remaining steps
|
|
520
|
+
if (!context.softAssertions) {
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
441
523
|
}
|
|
442
524
|
}
|
|
525
|
+
// Check for soft assertion errors at the end of the test
|
|
526
|
+
collectedSoftErrors = context.softAssertionErrors || [];
|
|
527
|
+
if (collectedSoftErrors.length > 0 && testStatus === 'passed') {
|
|
528
|
+
testStatus = 'failed';
|
|
529
|
+
const errorMessages = collectedSoftErrors.map((e, i) => ` ${i + 1}. ${e.message}`).join('\n');
|
|
530
|
+
testError = {
|
|
531
|
+
message: `${collectedSoftErrors.length} soft assertion(s) failed:\n${errorMessages}`,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
443
534
|
}
|
|
444
535
|
catch (error) {
|
|
445
536
|
testStatus = 'error';
|
|
@@ -457,6 +548,7 @@ class TestExecutor {
|
|
|
457
548
|
error: testError,
|
|
458
549
|
startedAt,
|
|
459
550
|
finishedAt: new Date().toISOString(),
|
|
551
|
+
softAssertionErrors: collectedSoftErrors.length > 0 ? collectedSoftErrors : undefined,
|
|
460
552
|
};
|
|
461
553
|
// Run afterTest hooks
|
|
462
554
|
for (const plugin of this.plugins.values()) {
|
|
@@ -577,6 +669,29 @@ class TestExecutor {
|
|
|
577
669
|
}
|
|
578
670
|
}
|
|
579
671
|
}
|
|
672
|
+
// Handle procedure calls (procedure_call block)
|
|
673
|
+
if (output && typeof output === 'object' && 'procedureCall' in output) {
|
|
674
|
+
const procOutput = output;
|
|
675
|
+
// Set procedure arguments as variables (resolve any ${var} placeholders first)
|
|
676
|
+
for (const [argName, argValue] of Object.entries(procOutput.args)) {
|
|
677
|
+
let resolvedValue = argValue;
|
|
678
|
+
if (typeof argValue === 'string') {
|
|
679
|
+
resolvedValue = TestExecutor.resolveVariablePlaceholders(argValue, context);
|
|
680
|
+
}
|
|
681
|
+
context.variables.set(argName, resolvedValue);
|
|
682
|
+
}
|
|
683
|
+
// Run the procedure's steps
|
|
684
|
+
if (procOutput.procedure.steps) {
|
|
685
|
+
for (const childStep of procOutput.procedure.steps) {
|
|
686
|
+
const childResult = await this.runStep(childStep, context);
|
|
687
|
+
if (childResult.status === 'failed' || childResult.status === 'error') {
|
|
688
|
+
status = childResult.status;
|
|
689
|
+
error = childResult.error;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
580
695
|
}
|
|
581
696
|
catch (err) {
|
|
582
697
|
status = 'failed';
|
|
@@ -618,13 +733,20 @@ class TestExecutor {
|
|
|
618
733
|
async resolveParams(params, context) {
|
|
619
734
|
const resolved = {};
|
|
620
735
|
for (const [key, value] of Object.entries(params)) {
|
|
621
|
-
if (value && typeof value === 'object' && 'type' in value) {
|
|
622
|
-
// This is a connected block - execute it to get the value
|
|
736
|
+
if (value && typeof value === 'object' && 'type' in value && 'id' in value) {
|
|
737
|
+
// This is a connected block (has both 'type' and 'id') - execute it to get the value
|
|
623
738
|
const nestedStep = value;
|
|
624
739
|
const blockDef = (0, core_1.getBlock)(nestedStep.type);
|
|
625
740
|
if (blockDef) {
|
|
626
741
|
const nestedParams = await this.resolveParams(nestedStep.params || {}, context);
|
|
627
|
-
|
|
742
|
+
const result = await blockDef.execute(nestedParams, context);
|
|
743
|
+
// Value blocks return { _value: actualValue, ... } - extract the actual value
|
|
744
|
+
if (result && typeof result === 'object' && '_value' in result) {
|
|
745
|
+
resolved[key] = result._value;
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
resolved[key] = result;
|
|
749
|
+
}
|
|
628
750
|
}
|
|
629
751
|
}
|
|
630
752
|
else {
|
|
@@ -683,6 +805,44 @@ class TestExecutor {
|
|
|
683
805
|
}
|
|
684
806
|
return resolved;
|
|
685
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* Resolve ${variable} placeholders in a string using context variables and currentData
|
|
810
|
+
*/
|
|
811
|
+
static resolveVariablePlaceholders(text, context) {
|
|
812
|
+
if (typeof text !== 'string')
|
|
813
|
+
return text;
|
|
814
|
+
return text.replace(/\$\{([\w.]+)\}/g, (match, path) => {
|
|
815
|
+
const parts = path.split('.');
|
|
816
|
+
const varName = parts[0];
|
|
817
|
+
// Check currentData first (for data-driven tests)
|
|
818
|
+
if (context.currentData?.values[varName] !== undefined) {
|
|
819
|
+
let value = context.currentData.values[varName];
|
|
820
|
+
// Handle dot notation for nested access
|
|
821
|
+
if (parts.length > 1 && value !== null && typeof value === 'object') {
|
|
822
|
+
for (let i = 1; i < parts.length; i++) {
|
|
823
|
+
if (value === undefined || value === null)
|
|
824
|
+
break;
|
|
825
|
+
value = value[parts[i]];
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (value !== undefined && value !== null) {
|
|
829
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Then check context variables
|
|
833
|
+
let value = context.variables.get(varName);
|
|
834
|
+
if (parts.length > 1 && value !== undefined && value !== null) {
|
|
835
|
+
for (let i = 1; i < parts.length; i++) {
|
|
836
|
+
if (value === undefined || value === null)
|
|
837
|
+
break;
|
|
838
|
+
value = value[parts[i]];
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (value === undefined || value === null)
|
|
842
|
+
return match;
|
|
843
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
686
846
|
createLogger() {
|
|
687
847
|
return {
|
|
688
848
|
info: (message, data) => {
|
package/dist/server/globals.d.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* - globals.json - Shared variables across all test files
|
|
6
6
|
* - snippets/ - Reusable block sequences (composite blocks)
|
|
7
7
|
*/
|
|
8
|
-
import { TestStep } from '../core';
|
|
8
|
+
import { TestStep, ProcedureDefinition } from '../core';
|
|
9
9
|
export interface GlobalsConfig {
|
|
10
10
|
variables?: Record<string, unknown>;
|
|
11
|
+
procedures?: Record<string, ProcedureDefinition>;
|
|
11
12
|
baseUrl?: string;
|
|
12
13
|
timeout?: number;
|
|
13
14
|
testIdAttribute?: string;
|
|
@@ -47,6 +48,10 @@ export declare function getGlobals(): GlobalsConfig;
|
|
|
47
48
|
* Get global variables
|
|
48
49
|
*/
|
|
49
50
|
export declare function getGlobalVariables(): Record<string, unknown>;
|
|
51
|
+
/**
|
|
52
|
+
* Get global procedures from globals.json
|
|
53
|
+
*/
|
|
54
|
+
export declare function getGlobalProcedures(): Record<string, ProcedureDefinition>;
|
|
50
55
|
/**
|
|
51
56
|
* Get the configured test ID attribute (defaults to 'data-testid')
|
|
52
57
|
*/
|
package/dist/server/globals.js
CHANGED
|
@@ -45,6 +45,7 @@ exports.getGlobalsDirectory = getGlobalsDirectory;
|
|
|
45
45
|
exports.loadGlobals = loadGlobals;
|
|
46
46
|
exports.getGlobals = getGlobals;
|
|
47
47
|
exports.getGlobalVariables = getGlobalVariables;
|
|
48
|
+
exports.getGlobalProcedures = getGlobalProcedures;
|
|
48
49
|
exports.getTestIdAttribute = getTestIdAttribute;
|
|
49
50
|
exports.setTestIdAttribute = setTestIdAttribute;
|
|
50
51
|
exports.discoverSnippets = discoverSnippets;
|
|
@@ -105,6 +106,12 @@ function getGlobals() {
|
|
|
105
106
|
function getGlobalVariables() {
|
|
106
107
|
return loadedGlobals.variables || {};
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Get global procedures from globals.json
|
|
111
|
+
*/
|
|
112
|
+
function getGlobalProcedures() {
|
|
113
|
+
return loadedGlobals.procedures || {};
|
|
114
|
+
}
|
|
108
115
|
/**
|
|
109
116
|
* Get the configured test ID attribute (defaults to 'data-testid')
|
|
110
117
|
*/
|