@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.
Files changed (71) hide show
  1. package/dist/cli/executor.d.ts +4 -1
  2. package/dist/cli/executor.js +101 -5
  3. package/dist/cli/index.js +148 -4
  4. package/dist/cli/reporters/ConsoleReporter.d.ts +12 -0
  5. package/dist/cli/reporters/ConsoleReporter.js +39 -0
  6. package/dist/cli/reporters/HTMLReporter.d.ts +19 -0
  7. package/dist/cli/reporters/HTMLReporter.js +506 -0
  8. package/dist/cli/reporters/JSONReporter.d.ts +15 -0
  9. package/dist/cli/reporters/JSONReporter.js +80 -0
  10. package/dist/cli/reporters/JUnitReporter.d.ts +19 -0
  11. package/dist/cli/reporters/JUnitReporter.js +105 -0
  12. package/dist/cli/reporters/index.d.ts +17 -0
  13. package/dist/cli/reporters/index.js +31 -0
  14. package/dist/cli/reporters/types.d.ts +28 -0
  15. package/dist/cli/reporters/types.js +2 -0
  16. package/dist/cli/reporters/utils.d.ts +31 -0
  17. package/dist/cli/reporters/utils.js +136 -0
  18. package/dist/cli/reporters.d.ts +13 -62
  19. package/dist/cli/reporters.js +16 -719
  20. package/dist/client/assets/index-Boo8ZrY_.js +2195 -0
  21. package/dist/client/assets/{index-dXniUrbi.js.map → index-Boo8ZrY_.js.map} +1 -1
  22. package/dist/client/assets/index-OxNH9dW-.css +1 -0
  23. package/dist/client/index.html +2 -2
  24. package/dist/core/blocks/api.js +3 -6
  25. package/dist/core/blocks/assertions.d.ts +31 -0
  26. package/dist/core/blocks/assertions.js +72 -0
  27. package/dist/core/blocks/index.d.ts +1 -0
  28. package/dist/core/blocks/index.js +6 -1
  29. package/dist/core/blocks/lifecycle.js +5 -3
  30. package/dist/core/blocks/logic.js +2 -3
  31. package/dist/core/blocks/playwright/assertions.d.ts +5 -0
  32. package/dist/core/blocks/playwright/assertions.js +321 -0
  33. package/dist/core/blocks/playwright/index.d.ts +17 -0
  34. package/dist/core/blocks/playwright/index.js +49 -0
  35. package/dist/core/blocks/playwright/interactions.d.ts +5 -0
  36. package/dist/core/blocks/playwright/interactions.js +191 -0
  37. package/dist/core/blocks/playwright/navigation.d.ts +5 -0
  38. package/dist/core/blocks/playwright/navigation.js +133 -0
  39. package/dist/core/blocks/playwright/retrieval.d.ts +5 -0
  40. package/dist/core/blocks/playwright/retrieval.js +144 -0
  41. package/dist/core/blocks/playwright/types.d.ts +65 -0
  42. package/dist/core/blocks/playwright/types.js +5 -0
  43. package/dist/core/blocks/playwright/utils.d.ts +26 -0
  44. package/dist/core/blocks/playwright/utils.js +137 -0
  45. package/dist/core/blocks/playwright.d.ts +13 -2
  46. package/dist/core/blocks/playwright.js +14 -761
  47. package/dist/core/executor/BaseTestExecutor.d.ts +60 -0
  48. package/dist/core/executor/BaseTestExecutor.js +297 -0
  49. package/dist/core/executor/index.d.ts +1 -0
  50. package/dist/core/executor/index.js +5 -0
  51. package/dist/core/index.d.ts +1 -0
  52. package/dist/core/index.js +4 -0
  53. package/dist/core/types.d.ts +12 -0
  54. package/dist/core/utils/blocklyParser.d.ts +18 -0
  55. package/dist/core/utils/blocklyParser.js +84 -0
  56. package/dist/core/utils/dataLoader.d.ts +9 -0
  57. package/dist/core/utils/dataLoader.js +117 -0
  58. package/dist/core/utils/index.d.ts +2 -0
  59. package/dist/core/utils/index.js +12 -0
  60. package/dist/core/utils/logger.d.ts +14 -0
  61. package/dist/core/utils/logger.js +48 -0
  62. package/dist/core/utils/variableResolver.d.ts +24 -0
  63. package/dist/core/utils/variableResolver.js +92 -0
  64. package/dist/server/executor.d.ts +6 -0
  65. package/dist/server/executor.js +207 -47
  66. package/dist/server/globals.d.ts +6 -1
  67. package/dist/server/globals.js +7 -0
  68. package/dist/server/startServer.js +15 -0
  69. package/package.json +1 -1
  70. package/dist/client/assets/index-dXniUrbi.js +0 -2193
  71. package/dist/client/assets/index-oTTttNKd.css +0 -1
@@ -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
  }
@@ -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
- // Set up parameter variables with prefix
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
- context.variables.set(`__param_${key}`, value);
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
- // Clean up parameter variables
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
- context.variables.delete(`__param_${key}`);
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('1.0.0');
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 it exists
133
+ // Load globals.json - search up directory tree from first test file if not explicitly specified
77
134
  let globalVariables = {};
78
- const globalsPath = path.resolve(options.globals);
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.6.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;