@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
package/dist/cli/executor.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { TestFile, TestCase, TestResult, Plugin, TestDataSet } from '../core';
|
|
1
|
+
import { TestFile, TestCase, TestResult, Plugin, ProcedureDefinition, TestDataSet } from '../core';
|
|
2
2
|
export interface ExecutorOptions {
|
|
3
3
|
headless?: boolean;
|
|
4
4
|
timeout?: number;
|
|
5
5
|
baseUrl?: string;
|
|
6
6
|
variables?: Record<string, unknown>;
|
|
7
7
|
plugins?: Plugin[];
|
|
8
|
+
procedures?: Record<string, ProcedureDefinition>;
|
|
8
9
|
}
|
|
9
10
|
export declare class TestExecutor {
|
|
10
11
|
private options;
|
|
@@ -23,11 +24,13 @@ export declare class TestExecutor {
|
|
|
23
24
|
runTest(test: TestCase, testFile: TestFile): Promise<TestResult>;
|
|
24
25
|
private runSteps;
|
|
25
26
|
private resolveVariableDefaults;
|
|
27
|
+
private registerCustomBlocksFromProcedures;
|
|
26
28
|
private extractStepsFromBlocklyState;
|
|
27
29
|
private blocksToSteps;
|
|
28
30
|
private blockToStep;
|
|
29
31
|
private runStep;
|
|
30
32
|
private executeProcedure;
|
|
33
|
+
private resolveVariablePlaceholders;
|
|
31
34
|
private resolveParams;
|
|
32
35
|
private createLogger;
|
|
33
36
|
}
|
package/dist/cli/executor.js
CHANGED
|
@@ -20,6 +20,13 @@ class TestExecutor {
|
|
|
20
20
|
this.plugins.set(plugin.name, plugin);
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
+
// Register project-level procedures from options
|
|
24
|
+
if (options.procedures) {
|
|
25
|
+
for (const [name, procedure] of Object.entries(options.procedures)) {
|
|
26
|
+
this.procedures.set(name, procedure);
|
|
27
|
+
}
|
|
28
|
+
this.registerCustomBlocksFromProcedures(options.procedures);
|
|
29
|
+
}
|
|
23
30
|
}
|
|
24
31
|
async initialize() {
|
|
25
32
|
this.browser = await playwright_1.chromium.launch({
|
|
@@ -81,6 +88,8 @@ class TestExecutor {
|
|
|
81
88
|
for (const [name, procedure] of Object.entries(testFile.procedures)) {
|
|
82
89
|
this.procedures.set(name, procedure);
|
|
83
90
|
}
|
|
91
|
+
// Also register them as custom blocks so custom_xxx blocks work
|
|
92
|
+
this.registerCustomBlocksFromProcedures(testFile.procedures);
|
|
84
93
|
}
|
|
85
94
|
// Create base context for hooks
|
|
86
95
|
const baseContext = this.createBaseContext(testFile.variables);
|
|
@@ -122,7 +131,7 @@ class TestExecutor {
|
|
|
122
131
|
return {
|
|
123
132
|
variables: new Map(Object.entries({
|
|
124
133
|
...this.resolveVariableDefaults(fileVariables),
|
|
125
|
-
...this.options.variables,
|
|
134
|
+
...this.resolveVariableDefaults(this.options.variables),
|
|
126
135
|
})),
|
|
127
136
|
results: [],
|
|
128
137
|
browser: this.browser,
|
|
@@ -347,6 +356,56 @@ class TestExecutor {
|
|
|
347
356
|
}
|
|
348
357
|
return resolved;
|
|
349
358
|
}
|
|
359
|
+
registerCustomBlocksFromProcedures(procedures) {
|
|
360
|
+
Object.entries(procedures).forEach(([_name, proc]) => {
|
|
361
|
+
if (!proc.steps || proc.steps.length === 0)
|
|
362
|
+
return;
|
|
363
|
+
const blockType = `custom_${proc.name.toLowerCase().replace(/\s+/g, '_')}`;
|
|
364
|
+
// Check if already registered
|
|
365
|
+
if ((0, core_1.getBlock)(blockType))
|
|
366
|
+
return;
|
|
367
|
+
const blockDef = {
|
|
368
|
+
type: blockType,
|
|
369
|
+
category: 'Custom',
|
|
370
|
+
color: '#607D8B',
|
|
371
|
+
tooltip: proc.description || `Custom block: ${proc.name}`,
|
|
372
|
+
inputs: (proc.params || []).map(param => ({
|
|
373
|
+
name: param.name.toUpperCase(),
|
|
374
|
+
type: 'field',
|
|
375
|
+
fieldType: param.type === 'number' ? 'number' : 'text',
|
|
376
|
+
default: param.default,
|
|
377
|
+
})),
|
|
378
|
+
previousStatement: true,
|
|
379
|
+
nextStatement: true,
|
|
380
|
+
execute: async (params, context) => {
|
|
381
|
+
context.logger.info(`Executing custom block: ${proc.name}`);
|
|
382
|
+
// Set procedure parameters in context.variables so ${paramName} references work
|
|
383
|
+
// Resolve any ${variable} placeholders in the parameter values first
|
|
384
|
+
(proc.params || []).forEach(p => {
|
|
385
|
+
const paramKey = p.name.toUpperCase();
|
|
386
|
+
let value = params[paramKey];
|
|
387
|
+
if (value !== undefined) {
|
|
388
|
+
// Resolve variable placeholders like ${email} from context
|
|
389
|
+
if (typeof value === 'string') {
|
|
390
|
+
value = this.resolveVariablePlaceholders(value, context);
|
|
391
|
+
}
|
|
392
|
+
context.variables.set(p.name, value);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
// Execute the procedure's steps directly
|
|
396
|
+
const steps = this.extractStepsFromBlocklyState(proc.steps);
|
|
397
|
+
for (const step of steps) {
|
|
398
|
+
const result = await this.runStep(step, context);
|
|
399
|
+
if (result.status !== 'passed') {
|
|
400
|
+
throw new Error(`Procedure ${proc.name} failed: ${result.error?.message}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { customBlock: true, name: proc.name };
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
(0, core_1.registerBlock)(blockDef);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
350
409
|
extractStepsFromBlocklyState(state) {
|
|
351
410
|
if (!state || typeof state !== 'object')
|
|
352
411
|
return [];
|
|
@@ -482,9 +541,20 @@ class TestExecutor {
|
|
|
482
541
|
throw new Error(`Procedure not found: ${name}`);
|
|
483
542
|
}
|
|
484
543
|
context.logger.info(` Executing procedure: ${name}`);
|
|
485
|
-
//
|
|
544
|
+
// Save original variable values to restore after procedure execution
|
|
545
|
+
const savedValues = new Map();
|
|
546
|
+
// Set up parameter variables (procedure steps reference them directly as ${paramName})
|
|
486
547
|
for (const [key, value] of Object.entries(args)) {
|
|
487
|
-
|
|
548
|
+
// Resolve any ${variable} placeholders in the argument value
|
|
549
|
+
let resolvedValue = value;
|
|
550
|
+
if (typeof value === 'string' && value.includes('${')) {
|
|
551
|
+
resolvedValue = this.resolveVariablePlaceholders(value, context);
|
|
552
|
+
}
|
|
553
|
+
// Save original value if it exists
|
|
554
|
+
if (context.variables.has(key)) {
|
|
555
|
+
savedValues.set(key, context.variables.get(key));
|
|
556
|
+
}
|
|
557
|
+
context.variables.set(key, resolvedValue);
|
|
488
558
|
}
|
|
489
559
|
// Execute procedure steps
|
|
490
560
|
const steps = this.extractStepsFromBlocklyState(procedure.steps);
|
|
@@ -503,12 +573,38 @@ class TestExecutor {
|
|
|
503
573
|
}
|
|
504
574
|
}
|
|
505
575
|
}
|
|
506
|
-
//
|
|
576
|
+
// Restore original variable values
|
|
577
|
+
for (const [key, originalValue] of savedValues) {
|
|
578
|
+
context.variables.set(key, originalValue);
|
|
579
|
+
}
|
|
580
|
+
// Remove variables that didn't exist before
|
|
507
581
|
for (const key of Object.keys(args)) {
|
|
508
|
-
|
|
582
|
+
if (!savedValues.has(key)) {
|
|
583
|
+
context.variables.delete(key);
|
|
584
|
+
}
|
|
509
585
|
}
|
|
510
586
|
return returnValue;
|
|
511
587
|
}
|
|
588
|
+
resolveVariablePlaceholders(text, context) {
|
|
589
|
+
return text.replace(/\$\{([\w.]+)\}/g, (match, path) => {
|
|
590
|
+
const parts = path.split('.');
|
|
591
|
+
const varName = parts[0];
|
|
592
|
+
let value = context.variables.get(varName);
|
|
593
|
+
// Handle dot notation for nested object access
|
|
594
|
+
if (parts.length > 1 && value !== undefined && value !== null) {
|
|
595
|
+
for (let i = 1; i < parts.length; i++) {
|
|
596
|
+
if (value === undefined || value === null)
|
|
597
|
+
break;
|
|
598
|
+
value = value[parts[i]];
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (value === undefined || value === null)
|
|
602
|
+
return match;
|
|
603
|
+
if (typeof value === 'object')
|
|
604
|
+
return JSON.stringify(value);
|
|
605
|
+
return String(value);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
512
608
|
async resolveParams(params, context) {
|
|
513
609
|
const resolved = {};
|
|
514
610
|
for (const [key, value] of Object.entries(params)) {
|
package/dist/cli/index.js
CHANGED
|
@@ -41,11 +41,67 @@ const glob_1 = require("glob");
|
|
|
41
41
|
const executor_1 = require("./executor");
|
|
42
42
|
const reporters_1 = require("./reporters");
|
|
43
43
|
const startServer_1 = require("../server/startServer");
|
|
44
|
+
const plugins_1 = require("../server/plugins");
|
|
45
|
+
const globals_1 = require("../server/globals");
|
|
46
|
+
/**
|
|
47
|
+
* Get the package version from package.json
|
|
48
|
+
*/
|
|
49
|
+
function getVersion() {
|
|
50
|
+
try {
|
|
51
|
+
// Try to find package.json relative to the compiled CLI
|
|
52
|
+
const possiblePaths = [
|
|
53
|
+
path.join(__dirname, '../../package.json'), // dist/cli -> package.json
|
|
54
|
+
path.join(__dirname, '../../../package.json'), // nested node_modules
|
|
55
|
+
];
|
|
56
|
+
for (const pkgPath of possiblePaths) {
|
|
57
|
+
if (fs.existsSync(pkgPath)) {
|
|
58
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
59
|
+
if (pkg.name === '@testsmith/testblocks' && pkg.version) {
|
|
60
|
+
return pkg.version;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Ignore errors
|
|
67
|
+
}
|
|
68
|
+
return '0.0.0';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Search up the directory tree for globals.json starting from the given directory
|
|
72
|
+
*/
|
|
73
|
+
function findGlobalsFile(startDir) {
|
|
74
|
+
let currentDir = path.resolve(startDir);
|
|
75
|
+
const root = path.parse(currentDir).root;
|
|
76
|
+
while (currentDir !== root) {
|
|
77
|
+
const globalsPath = path.join(currentDir, 'globals.json');
|
|
78
|
+
if (fs.existsSync(globalsPath)) {
|
|
79
|
+
return globalsPath;
|
|
80
|
+
}
|
|
81
|
+
currentDir = path.dirname(currentDir);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Search up the directory tree for a plugins directory starting from the given directory
|
|
87
|
+
*/
|
|
88
|
+
function findPluginsDir(startDir) {
|
|
89
|
+
let currentDir = path.resolve(startDir);
|
|
90
|
+
const root = path.parse(currentDir).root;
|
|
91
|
+
while (currentDir !== root) {
|
|
92
|
+
const pluginsPath = path.join(currentDir, 'plugins');
|
|
93
|
+
if (fs.existsSync(pluginsPath) && fs.statSync(pluginsPath).isDirectory()) {
|
|
94
|
+
return pluginsPath;
|
|
95
|
+
}
|
|
96
|
+
currentDir = path.dirname(currentDir);
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
44
100
|
const program = new commander_1.Command();
|
|
45
101
|
program
|
|
46
102
|
.name('testblocks')
|
|
47
103
|
.description('CLI runner for TestBlocks visual test automation')
|
|
48
|
-
.version(
|
|
104
|
+
.version(getVersion());
|
|
49
105
|
program
|
|
50
106
|
.command('run')
|
|
51
107
|
.description('Run test files')
|
|
@@ -57,6 +113,7 @@ program
|
|
|
57
113
|
.option('-b, --base-url <url>', 'Base URL for relative URLs')
|
|
58
114
|
.option('-v, --var <vars...>', 'Variables in key=value format')
|
|
59
115
|
.option('-g, --globals <path>', 'Path to globals.json file', './globals.json')
|
|
116
|
+
.option('--plugins-dir <dir>', 'Plugins directory (auto-discovered if not specified)')
|
|
60
117
|
.option('--fail-fast', 'Stop on first test failure', false)
|
|
61
118
|
.option('-p, --parallel <count>', 'Number of parallel workers', '1')
|
|
62
119
|
.option('--filter <pattern>', 'Only run tests matching pattern')
|
|
@@ -73,9 +130,15 @@ program
|
|
|
73
130
|
process.exit(1);
|
|
74
131
|
}
|
|
75
132
|
console.log(`Found ${files.length} test file(s)\n`);
|
|
76
|
-
// Load globals.json if
|
|
133
|
+
// Load globals.json - search up directory tree from first test file if not explicitly specified
|
|
77
134
|
let globalVariables = {};
|
|
78
|
-
|
|
135
|
+
let globalProcedures = {};
|
|
136
|
+
let globalsPath = path.resolve(options.globals);
|
|
137
|
+
// If default globals path doesn't exist, search up from first test file
|
|
138
|
+
if (!fs.existsSync(globalsPath) && options.globals === './globals.json' && files.length > 0) {
|
|
139
|
+
const testDir = path.dirname(files[0]);
|
|
140
|
+
globalsPath = findGlobalsFile(testDir) || globalsPath;
|
|
141
|
+
}
|
|
79
142
|
if (fs.existsSync(globalsPath)) {
|
|
80
143
|
try {
|
|
81
144
|
const globalsContent = fs.readFileSync(globalsPath, 'utf-8');
|
|
@@ -83,11 +146,41 @@ program
|
|
|
83
146
|
if (globals.variables && typeof globals.variables === 'object') {
|
|
84
147
|
globalVariables = globals.variables;
|
|
85
148
|
}
|
|
149
|
+
if (globals.procedures && typeof globals.procedures === 'object') {
|
|
150
|
+
globalProcedures = globals.procedures;
|
|
151
|
+
}
|
|
86
152
|
}
|
|
87
153
|
catch (e) {
|
|
88
154
|
console.warn(`Warning: Could not load globals from ${globalsPath}: ${e.message}`);
|
|
89
155
|
}
|
|
90
156
|
}
|
|
157
|
+
// Load plugins - search up directory tree from first test file if not explicitly specified
|
|
158
|
+
let pluginsDir = options.pluginsDir ? path.resolve(options.pluginsDir) : null;
|
|
159
|
+
// If plugins dir not specified, auto-discover from test file location or globals location
|
|
160
|
+
if (!pluginsDir && files.length > 0) {
|
|
161
|
+
const testDir = path.dirname(files[0]);
|
|
162
|
+
pluginsDir = findPluginsDir(testDir);
|
|
163
|
+
}
|
|
164
|
+
// Also check next to globals.json if still not found
|
|
165
|
+
if (!pluginsDir && fs.existsSync(globalsPath)) {
|
|
166
|
+
const globalsDir = path.dirname(globalsPath);
|
|
167
|
+
const pluginsDirNextToGlobals = path.join(globalsDir, 'plugins');
|
|
168
|
+
if (fs.existsSync(pluginsDirNextToGlobals) && fs.statSync(pluginsDirNextToGlobals).isDirectory()) {
|
|
169
|
+
pluginsDir = pluginsDirNextToGlobals;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Load plugins if directory found
|
|
173
|
+
if (pluginsDir && fs.existsSync(pluginsDir)) {
|
|
174
|
+
(0, plugins_1.setPluginsDirectory)(pluginsDir);
|
|
175
|
+
await (0, plugins_1.loadAllPlugins)();
|
|
176
|
+
(0, plugins_1.initializeServerPlugins)();
|
|
177
|
+
}
|
|
178
|
+
// Load snippets from the globals directory (snippets/ folder next to globals.json)
|
|
179
|
+
if (fs.existsSync(globalsPath)) {
|
|
180
|
+
const globalsDir = path.dirname(globalsPath);
|
|
181
|
+
(0, globals_1.setGlobalsDirectory)(globalsDir);
|
|
182
|
+
(0, globals_1.loadAllSnippets)();
|
|
183
|
+
}
|
|
91
184
|
// Parse CLI variables (these override globals)
|
|
92
185
|
const cliVariables = {};
|
|
93
186
|
if (options.var) {
|
|
@@ -111,6 +204,7 @@ program
|
|
|
111
204
|
timeout: parseInt(options.timeout, 10),
|
|
112
205
|
baseUrl: options.baseUrl,
|
|
113
206
|
variables,
|
|
207
|
+
procedures: globalProcedures,
|
|
114
208
|
};
|
|
115
209
|
// Create reporter
|
|
116
210
|
const reporter = createReporter(options.reporter, options.output);
|
|
@@ -248,7 +342,7 @@ program
|
|
|
248
342
|
'test:junit': 'testblocks run tests/**/*.testblocks.json -r junit -o reports',
|
|
249
343
|
},
|
|
250
344
|
devDependencies: {
|
|
251
|
-
'@testsmith/testblocks': '^0.
|
|
345
|
+
'@testsmith/testblocks': '^0.8.0',
|
|
252
346
|
},
|
|
253
347
|
};
|
|
254
348
|
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
|
|
@@ -354,6 +448,56 @@ Thumbs.db
|
|
|
354
448
|
fs.writeFileSync(gitignorePath, gitignore);
|
|
355
449
|
console.log(' Created: .gitignore');
|
|
356
450
|
}
|
|
451
|
+
// Create GitHub Actions workflow
|
|
452
|
+
const workflowDir = path.join(projectDir, '.github', 'workflows');
|
|
453
|
+
if (!fs.existsSync(workflowDir)) {
|
|
454
|
+
fs.mkdirSync(workflowDir, { recursive: true });
|
|
455
|
+
console.log(' Created: .github/workflows/');
|
|
456
|
+
}
|
|
457
|
+
const workflowPath = path.join(workflowDir, 'testblocks.yml');
|
|
458
|
+
if (!fs.existsSync(workflowPath)) {
|
|
459
|
+
const workflow = `name: TestBlocks Tests
|
|
460
|
+
|
|
461
|
+
on:
|
|
462
|
+
push:
|
|
463
|
+
branches: [main, master]
|
|
464
|
+
pull_request:
|
|
465
|
+
branches: [main, master]
|
|
466
|
+
|
|
467
|
+
jobs:
|
|
468
|
+
test:
|
|
469
|
+
runs-on: ubuntu-latest
|
|
470
|
+
|
|
471
|
+
steps:
|
|
472
|
+
- name: Checkout repository
|
|
473
|
+
uses: actions/checkout@v4
|
|
474
|
+
|
|
475
|
+
- name: Setup Node.js
|
|
476
|
+
uses: actions/setup-node@v4
|
|
477
|
+
with:
|
|
478
|
+
node-version: '20'
|
|
479
|
+
cache: 'npm'
|
|
480
|
+
|
|
481
|
+
- name: Install dependencies
|
|
482
|
+
run: npm ci
|
|
483
|
+
|
|
484
|
+
- name: Install Playwright browsers
|
|
485
|
+
run: npx playwright install --with-deps chromium
|
|
486
|
+
|
|
487
|
+
- name: Run tests
|
|
488
|
+
run: npm test
|
|
489
|
+
|
|
490
|
+
- name: Upload test reports
|
|
491
|
+
uses: actions/upload-artifact@v4
|
|
492
|
+
if: always()
|
|
493
|
+
with:
|
|
494
|
+
name: test-reports
|
|
495
|
+
path: reports/
|
|
496
|
+
retention-days: 30
|
|
497
|
+
`;
|
|
498
|
+
fs.writeFileSync(workflowPath, workflow);
|
|
499
|
+
console.log(' Created: .github/workflows/testblocks.yml');
|
|
500
|
+
}
|
|
357
501
|
console.log('\n✓ Project initialized successfully!\n');
|
|
358
502
|
console.log('Next steps:');
|
|
359
503
|
console.log(' 1. cd ' + (directory === '.' ? '' : directory));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TestFile, TestResult } from '../../core';
|
|
2
|
+
import { Reporter } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Console reporter - outputs results to the terminal
|
|
5
|
+
*/
|
|
6
|
+
export declare class ConsoleReporter implements Reporter {
|
|
7
|
+
onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
|
|
8
|
+
onComplete(allResults: {
|
|
9
|
+
file: string;
|
|
10
|
+
results: TestResult[];
|
|
11
|
+
}[]): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConsoleReporter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Console reporter - outputs results to the terminal
|
|
6
|
+
*/
|
|
7
|
+
class ConsoleReporter {
|
|
8
|
+
onTestFileComplete(file, testFile, results) {
|
|
9
|
+
console.log('');
|
|
10
|
+
for (const result of results) {
|
|
11
|
+
const icon = result.status === 'passed' ? '✓' : '✗';
|
|
12
|
+
const color = result.status === 'passed' ? '\x1b[32m' : '\x1b[31m';
|
|
13
|
+
const reset = '\x1b[0m';
|
|
14
|
+
console.log(`${color} ${icon} ${result.testName}${reset} (${result.duration}ms)`);
|
|
15
|
+
if (result.error) {
|
|
16
|
+
console.log(` ${result.error.message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
console.log('');
|
|
20
|
+
}
|
|
21
|
+
onComplete(allResults) {
|
|
22
|
+
const totalTests = allResults.reduce((sum, r) => sum + r.results.length, 0);
|
|
23
|
+
const passed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0);
|
|
24
|
+
const failed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0);
|
|
25
|
+
const totalDuration = allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0);
|
|
26
|
+
console.log('─'.repeat(50));
|
|
27
|
+
console.log(`Tests: ${passed} passed, ${failed} failed, ${totalTests} total`);
|
|
28
|
+
console.log(`Duration: ${(totalDuration / 1000).toFixed(2)}s`);
|
|
29
|
+
console.log(`Test Files: ${allResults.length}`);
|
|
30
|
+
console.log('─'.repeat(50));
|
|
31
|
+
if (failed > 0) {
|
|
32
|
+
console.log('\n\x1b[31mTest run failed\x1b[0m\n');
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.log('\n\x1b[32mAll tests passed!\x1b[0m\n');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.ConsoleReporter = ConsoleReporter;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TestFile, TestResult } from '../../core';
|
|
2
|
+
import { Reporter, ReportData } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* HTML reporter - generates a styled HTML report
|
|
5
|
+
*/
|
|
6
|
+
export declare class HTMLReporter implements Reporter {
|
|
7
|
+
private outputDir;
|
|
8
|
+
private allResults;
|
|
9
|
+
constructor(outputDir: string);
|
|
10
|
+
onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
|
|
11
|
+
onComplete(allResults: {
|
|
12
|
+
file: string;
|
|
13
|
+
results: TestResult[];
|
|
14
|
+
}[]): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate HTML report from report data
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateHTMLReport(data: ReportData): string;
|