testaro 4.3.1 → 4.4.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/commands.js CHANGED
@@ -1,7 +1,7 @@
1
1
  exports.commands = {
2
2
  etc: {
3
3
  button: [
4
- 'Click a button',
4
+ 'Click a button or submit input',
5
5
  {
6
6
  which: [true, 'string', 'hasLength', 'substring of button text'],
7
7
  index: [false, 'number', '', 'index among matches if not 0'],
@@ -94,7 +94,7 @@ exports.commands = {
94
94
  {
95
95
  which: [true, 'string', 'hasLength', 'substring of select-list text'],
96
96
  index: [false, 'number', '', 'index among matches if not 0'],
97
- what: [true, 'string', 'hasLength', 'substring of option text']
97
+ what: [true, 'string', 'hasLength', 'substring of option text content']
98
98
  }
99
99
  ],
100
100
  state: [
package/high.js CHANGED
@@ -1,5 +1,8 @@
1
- // high.js
2
- // Invokes Testaro with the high-level method.
1
+ /*
2
+ high.js
3
+ Invokes Testaro with the high-level method.
4
+ Usage example: node high tp10 weborgs
5
+ */
3
6
 
4
7
  const {runJob} = require('./create');
5
8
  const scriptID = process.argv[2];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "4.3.1",
3
+ "version": "4.4.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -12,12 +12,13 @@ const {commands} = require('./commands');
12
12
  const debug = process.env.DEBUG === 'true';
13
13
  // Set WAITS environment variable to a positive number to insert delays (in ms).
14
14
  const waits = Number.parseInt(process.env.WAITS) || 0;
15
+ const urlInject = process.env.URL_INJECT || 'yes';
15
16
  // CSS selectors for targets of moves.
16
17
  const moves = {
17
- button: 'button',
18
+ button: 'button, [role=button], input[type=submit]',
18
19
  checkbox: 'input[type=checkbox]',
19
20
  focus: true,
20
- link: 'a',
21
+ link: 'a, [role=link]',
21
22
  radio: 'input[type=radio]',
22
23
  select: 'select',
23
24
  text: 'input[type=text]'
@@ -291,7 +292,13 @@ const textOf = async (page, element) => {
291
292
  // Return its visible labels, descriptions, and legend if the first input in a fieldset.
292
293
  totalText = await page.evaluate(element => {
293
294
  const {tagName} = element;
294
- const ownText = ['A', 'BUTTON'].includes(tagName) ? element.textContent : '';
295
+ let ownText = '';
296
+ if (['A', 'BUTTON'].includes(tagName)) {
297
+ ownText = element.textContent;
298
+ }
299
+ else if (tagName === 'INPUT' && element.type === 'submit') {
300
+ ownText = element.value;
301
+ }
295
302
  // HTML link elements have no labels property.
296
303
  const labels = tagName !== 'A' ? Array.from(element.labels) : [];
297
304
  const labelTexts = labels.map(label => label.textContent);
@@ -377,10 +384,10 @@ const matchElement = async (page, selector, matchText, index = 0) => {
377
384
  return ! matchText || bodyText.includes(matchText);
378
385
  },
379
386
  [slimText, slimBody],
380
- {timeout: 2000}
387
+ {timeout: 4000}
381
388
  )
382
389
  .catch(async error => {
383
- console.log(`ERROR: text to match not in body (${error.message})`);
390
+ console.log(`ERROR: text “${matchText}” not in body (${error.message})`);
384
391
  });
385
392
  // If there is no text to be matched or the body contained it:
386
393
  if (textInBodyJSHandle) {
@@ -549,7 +556,7 @@ const isTrue = (object, specs) => {
549
556
  }
550
557
  return [actual, satisfied];
551
558
  };
552
- // Adds an error result to an act.
559
+ // Adds a wait error result to an act.
553
560
  const waitError = (page, act, error, what) => {
554
561
  console.log(`ERROR waiting for ${what} (${error.message})`);
555
562
  act.result = {url: page.url()};
@@ -623,28 +630,44 @@ const doActs = async (report, actIndex, page) => {
623
630
  else if (act.type === 'wait') {
624
631
  const {what, which} = act;
625
632
  console.log(`>> for ${what} to include “${which}”`);
626
- // Wait 5 seconds for the specified text to appear in the specified place.
633
+ // Wait 5 or 10 seconds for the specified text, and quit if it does not.
627
634
  let successJSHandle;
628
635
  if (act.what === 'url') {
629
636
  successJSHandle = await page.waitForFunction(
630
637
  text => document.URL.includes(text), act.which, {timeout: 5000}
631
638
  )
632
- .catch(error => waitError(page, act, error, 'URL'));
639
+ .catch(error => {
640
+ actIndex = acts.length;
641
+ return waitError(page, act, error, 'URL');
642
+ });
633
643
  }
634
644
  else if (act.what === 'title') {
635
645
  successJSHandle = await page.waitForFunction(
636
646
  text => document.title.includes(text), act.which, {timeout: 5000}
637
647
  )
638
- .catch(error => waitError(page, act, error, 'title'));
648
+ .catch(error => {
649
+ actIndex = acts.length;
650
+ return waitError(page, act, error, 'title');
651
+ });
639
652
  }
640
653
  else if (act.what === 'body') {
641
654
  successJSHandle = await page.waitForFunction(
642
655
  matchText => {
643
- const innerText = document.body.innerText;
644
- return innerText.includes(matchText);
645
- }, which, {timeout: 5000}
656
+ const innerText = document && document.body && document.body.innerText;
657
+ if (innerText) {
658
+ return innerText.includes(matchText);
659
+ }
660
+ else {
661
+ actIndex = acts.length;
662
+ console.log('ERROR finding document body');
663
+ return false;
664
+ }
665
+ }, which, {timeout: 20000}
646
666
  )
647
- .catch(error => waitError(page, act, error, 'body'));
667
+ .catch(error => {
668
+ actIndex = acts.length;
669
+ return waitError(page, act, error, 'body');
670
+ });
648
671
  }
649
672
  if (successJSHandle) {
650
673
  act.result = {url: page.url()};
@@ -663,18 +686,20 @@ const doActs = async (report, actIndex, page) => {
663
686
  // If the state is valid:
664
687
  const stateIndex = ['loaded', 'idle'].indexOf(act.which);
665
688
  if (stateIndex !== -1) {
666
- // Wait for it.
689
+ // Wait for it, and quit if it does not appear.
667
690
  await page.waitForLoadState(
668
691
  ['domcontentloaded', 'networkidle'][stateIndex], {timeout: [10000, 5000][stateIndex]}
669
692
  )
670
693
  .catch(error => {
671
694
  console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
672
695
  act.result = `ERROR waiting for page to be ${act.which}`;
696
+ actIndex = acts.length;
673
697
  });
674
698
  }
675
699
  else {
676
700
  console.log('ERROR: invalid state');
677
701
  act.result = 'ERROR: invalid state';
702
+ actIndex = acts.length;
678
703
  }
679
704
  }
680
705
  // Otherwise, if the act is a page switch:
@@ -933,7 +958,13 @@ const doActs = async (report, actIndex, page) => {
933
958
  else if (act.type === 'link') {
934
959
  const href = await whichElement.getAttribute('href');
935
960
  const target = await whichElement.getAttribute('target');
936
- await whichElement.click({timeout: 2000});
961
+ await whichElement.click({timeout: 2000})
962
+ .catch(async () => {
963
+ await whichElement.click({
964
+ force: true,
965
+ timeout: 10000
966
+ });
967
+ });
937
968
  act.result = {
938
969
  href: href || 'NONE',
939
970
  target: target || 'NONE',
@@ -942,10 +973,21 @@ const doActs = async (report, actIndex, page) => {
942
973
  }
943
974
  // Otherwise, if it is selecting an option in a select list, perform it.
944
975
  else if (act.type === 'select') {
945
- await whichElement.selectOption({what: act.what});
946
- const optionText = await whichElement.$eval(
947
- 'option:selected', el => el.textContent
948
- );
976
+ const options = await whichElement.$$('option');
977
+ let optionText = '';
978
+ if (options && Array.isArray(options) && options.length) {
979
+ const optionTexts = [];
980
+ for (const option of options) {
981
+ const optionText = await option.textContent();
982
+ optionTexts.push(optionText);
983
+ }
984
+ const matchTexts = optionTexts.map((text, index) => text.includes(act.what) ? index : -1);
985
+ const index = matchTexts.filter(text => text > -1)[act.index || 0];
986
+ if (index !== undefined) {
987
+ await whichElement.selectOption({index});
988
+ optionText = optionTexts[index];
989
+ }
990
+ }
949
991
  act.result = optionText
950
992
  ? `“${optionText}}” selected`
951
993
  : 'ERROR: option not found';
@@ -1247,8 +1289,10 @@ exports.handleRequest = async report => {
1247
1289
  report.timeStamp = report.id.replace(/-.+/, '');
1248
1290
  // Add the script commands to the report as its initial acts.
1249
1291
  report.acts = JSON.parse(JSON.stringify(report.script.commands));
1250
- // Inject url acts where necessary to undo DOM changes.
1251
- injectURLActs(report.acts);
1292
+ // Inject url acts where necessary to undo DOM changes, if specified.
1293
+ if (urlInject === 'yes') {
1294
+ injectURLActs(report.acts);
1295
+ }
1252
1296
  // Perform the acts, asynchronously adding to the log and report.
1253
1297
  await doScript(report);
1254
1298
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "a11yorgs",
3
- "what": "Pages expected to pass all accessibility tests",
3
+ "what": "Home pages of accessibility organizations",
4
4
  "hosts": [
5
5
  {
6
6
  "id": "iaap",
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "eurail",
3
+ "what": "Eurail",
4
+ "hosts": [
5
+ {
6
+ "id": "eurail",
7
+ "which": "https://www.eurail.com/en",
8
+ "what": "Eurail"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,63 @@
1
+ {
2
+ "id": "digicert",
3
+ "what": "moves on DigiCert website",
4
+ "strict": true,
5
+ "commands": [
6
+ {
7
+ "type": "launch",
8
+ "which": "chromium",
9
+ "what": "Chrome"
10
+ },
11
+ {
12
+ "type": "url",
13
+ "which": "https://order.digicert.com/",
14
+ "what": "DigiCert"
15
+ },
16
+ {
17
+ "type": "focus",
18
+ "what": "button",
19
+ "which": "Choose payment plan"
20
+ },
21
+ {
22
+ "type": "press",
23
+ "which": "ArrowDown",
24
+ "again": 2,
25
+ "what": "Choose 2-year plan"
26
+ },
27
+ {
28
+ "type": "press",
29
+ "which": "Enter",
30
+ "what": "Commit to choose 2-year plan"
31
+ },
32
+ {
33
+ "type": "button",
34
+ "which": "I don't have",
35
+ "what": "I don’t have my CSR"
36
+ },
37
+ {
38
+ "type": "select",
39
+ "which": "Server app details",
40
+ "what": "NGINX"
41
+ },
42
+ {
43
+ "type": "text",
44
+ "which": "main website",
45
+ "what": "jpdev.pro"
46
+ },
47
+ {
48
+ "type": "button",
49
+ "which": "Continue",
50
+ "what": "Continue"
51
+ },
52
+ {
53
+ "type": "wait",
54
+ "what": "url",
55
+ "which": "step2"
56
+ },
57
+ {
58
+ "type": "test",
59
+ "which": "bulk",
60
+ "what": "count of visible elements"
61
+ }
62
+ ]
63
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "ttp10",
2
+ "id": "tp10",
3
3
  "what": "AATT, Alfa, Axe, IBM, Tenon, WAVE, and 16 custom tests",
4
4
  "strict": true,
5
5
  "commands": [
package/tests/aatt.js CHANGED
@@ -53,13 +53,23 @@ exports.reporter = async (page, waitLong) => {
53
53
  }
54
54
  catch (error) {
55
55
  console.log(`ERROR processing AATT report (${error.message})`);
56
- return {result: 'ERROR processing AATT report'};
56
+ return {
57
+ result: {
58
+ prevented: true,
59
+ error: 'ERROR processing AATT report'
60
+ }
61
+ };
57
62
  }
58
63
  }
59
64
  // Otherwise, i.e. if the result did not arrive within the time limit:
60
65
  else {
61
66
  // Report the failure.
62
67
  console.log('ERROR: getting report took too long');
63
- return {result: 'ERROR: getting AATT report took too long'};
68
+ return {
69
+ result: {
70
+ prevented: true,
71
+ error: 'ERROR: getting AATT report took too long'
72
+ }
73
+ };
64
74
  }
65
75
  };
package/tests/alfa.js CHANGED
@@ -4,11 +4,13 @@
4
4
  */
5
5
 
6
6
  // IMPORTS
7
+
7
8
  const {Audit} = require('@siteimprove/alfa-act');
8
9
  const {Scraper} = require('@siteimprove/alfa-scraper');
9
10
  const alfaRules = require('@siteimprove/alfa-rules');
10
11
 
11
12
  // FUNCTIONS
13
+
12
14
  // Conducts and reports an alfa test.
13
15
  exports.reporter = async page => {
14
16
  // Get the document containing the summaries of the alfa rules.
@@ -104,6 +106,11 @@ exports.reporter = async page => {
104
106
  }
105
107
  catch(error) {
106
108
  console.log(`ERROR: navigation to URL timed out (${error})`);
107
- return {result: {error: 'ERROR: navigation to URL timed out'}};
109
+ return {
110
+ result: {
111
+ prevented: true,
112
+ error: 'ERROR: navigation to URL timed out'
113
+ }
114
+ };
108
115
  }
109
116
  };
package/tests/axe.js CHANGED
@@ -16,10 +16,11 @@ exports.reporter = async (page, withItems, rules = []) => {
16
16
  await injectAxe(page)
17
17
  .catch(error => {
18
18
  console.log(`ERROR: Axe injection failed (${error.message})`);
19
- data.result = 'ERROR: axe injection failed';
19
+ data.prevented = true;
20
+ data.error = 'ERROR: axe injection failed';
20
21
  });
21
22
  // If the injection succeeded:
22
- if (! data.result) {
23
+ if (! data.prevented) {
23
24
  // Get the data on the elements violating the specified axe-core rules.
24
25
  const axeOptions = {};
25
26
  if (rules.length) {
@@ -100,6 +101,7 @@ exports.reporter = async (page, withItems, rules = []) => {
100
101
  // Otherwise, i.e. if the test failed:
101
102
  else {
102
103
  // Report this.
104
+ data.prevented = true;
103
105
  data.error = 'ERROR: axe failed';
104
106
  console.log('ERROR: axe failed');
105
107
  }
package/tests/bulk.js CHANGED
@@ -12,6 +12,7 @@ exports.reporter = async page => {
12
12
  await page.waitForSelector('body', {timeout: 10000})
13
13
  .catch(error => {
14
14
  console.log(`ERROR (${error.message})`);
15
+ data.prevented = true;
15
16
  data.error = 'ERROR: bulk timed out';
16
17
  return {result: data};
17
18
  });
package/tests/focOp.js CHANGED
@@ -127,6 +127,7 @@ exports.reporter = async (page, withItems) => {
127
127
  }, withItems)
128
128
  .catch(error => {
129
129
  console.log(`ERROR getting focOp data (${error.message})`);
130
+ data.prevented = true;
130
131
  });
131
132
  return {result: data};
132
133
  };
package/tests/hover.js CHANGED
@@ -212,6 +212,7 @@ exports.reporter = async (page, withItems) => {
212
212
  const triggers = await page.$$(selectors.join(', '))
213
213
  .catch(error => {
214
214
  console.log(`ERROR getting hover triggers (${error.message})`);
215
+ data.prevented = true;
215
216
  return [];
216
217
  });
217
218
  // If they number more than the sample size limit, sample them.
package/tests/ibm.js CHANGED
@@ -48,10 +48,12 @@ const report = (ibmReport, withItems) => {
48
48
  }
49
49
  }
50
50
  else {
51
+ data.prevented = true;
51
52
  data.error = 'ERROR: ibm test delivered no totals';
52
53
  }
53
54
  }
54
55
  else {
56
+ data.prevented = true;
55
57
  data.error = 'ERROR: ibm test delivered no report summary';
56
58
  }
57
59
  return data;
@@ -88,6 +90,7 @@ const doTest = async (content, withItems, timeLimit) => {
88
90
  else {
89
91
  console.log('ERROR: getting ibm test report took too long');
90
92
  return {
93
+ prevented: true,
91
94
  error: 'ERROR: getting ibm test report took too long'
92
95
  };
93
96
  }
@@ -100,12 +103,18 @@ exports.reporter = async (page, withItems, withNewContent) => {
100
103
  const timeLimit = 15;
101
104
  const typeContent = await page.content();
102
105
  result.content = await doTest(typeContent, withItems, timeLimit);
106
+ if (result.content.prevented) {
107
+ result.prevented = true;
108
+ }
103
109
  }
104
110
  // If a test with new content is to be performed:
105
111
  if ([true, undefined].includes(withNewContent)) {
106
112
  const timeLimit = 20;
107
113
  const typeContent = page.url();
108
114
  result.url = await doTest(typeContent, withItems, timeLimit);
115
+ if (result.content.prevented) {
116
+ result.prevented = true;
117
+ }
109
118
  }
110
119
  return {result};
111
120
  };
package/tests/labClash.js CHANGED
@@ -151,7 +151,10 @@ exports.reporter = async (page, withItems) => {
151
151
  }, withItems)
152
152
  .catch(error => {
153
153
  console.log(`ERROR: labClash failed (${error.message})`);
154
- const data = {error: 'ERROR: labClash failed'};
154
+ const data = {
155
+ prevented: true,
156
+ error: 'ERROR: labClash failed'
157
+ };
155
158
  return {result: data};
156
159
  });
157
160
  };
package/tests/motion.js CHANGED
@@ -110,6 +110,11 @@ exports.reporter = async (page, delay, interval, count) => {
110
110
  // Otherwise, i.e. if the shooting failed:
111
111
  else {
112
112
  // Return failure.
113
- return {result: {error: 'ERROR: screenshots failed'}};
113
+ return {
114
+ result: {
115
+ prevented: true,
116
+ error: 'ERROR: screenshots failed'
117
+ }
118
+ };
114
119
  }
115
120
  };
@@ -11,11 +11,6 @@ exports.reporter = async (page, withItems) => {
11
11
  return await page.$eval('body', (body, args) => {
12
12
  const withItems = args[0];
13
13
  const linkTypes = args[1];
14
- // Initialize the data to be returned.
15
- const data = {totals: {}};
16
- if (withItems) {
17
- data.items = {};
18
- }
19
14
  // Identify the settable style properties to be compared for all tag names.
20
15
  const mainStyles = [
21
16
  'borderStyle',
@@ -39,6 +34,15 @@ exports.reporter = async (page, withItems) => {
39
34
  const headingStyles = [
40
35
  'fontSize'
41
36
  ];
37
+ // Initialize the data to be returned.
38
+ const data = {
39
+ mainStyles,
40
+ headingStyles,
41
+ totals: {}
42
+ };
43
+ if (withItems) {
44
+ data.items = {};
45
+ }
42
46
  // Identify the heading tag names to be analyzed.
43
47
  const headingNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
44
48
  // Identify the other nonlink tag names to be analyzed.
package/tests/tabNav.js CHANGED
@@ -3,66 +3,71 @@
3
3
  This test reports nonstandard keyboard navigation among tab elements in tab lists.
4
4
  Standards are based on https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel.
5
5
  */
6
- exports.reporter = async (page, withItems) => {
7
- // Initialize a report.
8
- const data = {
9
- totals: {
10
- navigations: {
11
- all: {
6
+
7
+ // CONSTANTS
8
+
9
+ const data = {
10
+ totals: {
11
+ navigations: {
12
+ all: {
13
+ total: 0,
14
+ correct: 0,
15
+ incorrect: 0
16
+ },
17
+ specific: {
18
+ tab: {
12
19
  total: 0,
13
20
  correct: 0,
14
21
  incorrect: 0
15
22
  },
16
- specific: {
17
- tab: {
18
- total: 0,
19
- correct: 0,
20
- incorrect: 0
21
- },
22
- left: {
23
- total: 0,
24
- correct: 0,
25
- incorrect: 0
26
- },
27
- right: {
28
- total: 0,
29
- correct: 0,
30
- incorrect: 0
31
- },
32
- up: {
33
- total: 0,
34
- correct: 0,
35
- incorrect: 0
36
- },
37
- down: {
38
- total: 0,
39
- correct: 0,
40
- incorrect: 0
41
- },
42
- home: {
43
- total: 0,
44
- correct: 0,
45
- incorrect: 0
46
- },
47
- end: {
48
- total: 0,
49
- correct: 0,
50
- incorrect: 0
51
- }
23
+ left: {
24
+ total: 0,
25
+ correct: 0,
26
+ incorrect: 0
27
+ },
28
+ right: {
29
+ total: 0,
30
+ correct: 0,
31
+ incorrect: 0
32
+ },
33
+ up: {
34
+ total: 0,
35
+ correct: 0,
36
+ incorrect: 0
37
+ },
38
+ down: {
39
+ total: 0,
40
+ correct: 0,
41
+ incorrect: 0
42
+ },
43
+ home: {
44
+ total: 0,
45
+ correct: 0,
46
+ incorrect: 0
47
+ },
48
+ end: {
49
+ total: 0,
50
+ correct: 0,
51
+ incorrect: 0
52
52
  }
53
- },
54
- tabElements: {
55
- total: 0,
56
- correct: 0,
57
- incorrect: 0
58
- },
59
- tabLists: {
60
- total: 0,
61
- correct: 0,
62
- incorrect: 0
63
53
  }
54
+ },
55
+ tabElements: {
56
+ total: 0,
57
+ correct: 0,
58
+ incorrect: 0
59
+ },
60
+ tabLists: {
61
+ total: 0,
62
+ correct: 0,
63
+ incorrect: 0
64
64
  }
65
- };
65
+ }
66
+ };
67
+
68
+ // FUNCTIONS
69
+
70
+ exports.reporter = async (page, withItems) => {
66
71
  if (withItems) {
67
72
  data.tabElements = {
68
73
  incorrect: [],
@@ -182,6 +187,7 @@ exports.reporter = async (page, withItems) => {
182
187
  .catch(error => {
183
188
  console.log(`ERROR: could not get tag name (${error.message})`);
184
189
  found = false;
190
+ data.prevented = true;
185
191
  return 'ERROR: not found';
186
192
  });
187
193
  if (found) {
@@ -260,7 +266,10 @@ exports.reporter = async (page, withItems) => {
260
266
  if (! orientation) {
261
267
  orientation = 'horizontal';
262
268
  }
263
- if (orientation !== 'ERROR') {
269
+ if (orientation === 'ERROR') {
270
+ data.prevented = true;
271
+ }
272
+ else {
264
273
  const tabs = await firstTabList.$$('[role=tab]');
265
274
  // If the tablist contains at least 2 tab elements:
266
275
  if (tabs.length > 1) {
package/tests/tenon.js CHANGED
@@ -107,15 +107,19 @@ exports.reporter = async (tenonData, id) => {
107
107
  // Otherwise, if the test is still running after a wait for its status:
108
108
  else {
109
109
  // Report the test status.
110
- return {result: {
111
- error: 'ERROR: test status not completed',
112
- testStatus
113
- }};
110
+ return {
111
+ result: {
112
+ prevented: true,
113
+ error: 'ERROR: test status not completed',
114
+ testStatus
115
+ }
116
+ };
114
117
  }
115
118
  }
116
119
  else {
117
120
  return {
118
121
  result: {
122
+ prevented: true,
119
123
  error: 'ERROR: tenon authorization and test data incomplete'
120
124
  }
121
125
  };
package/tests/wave.js CHANGED
@@ -32,6 +32,7 @@ exports.reporter = async (page, reportType) => {
32
32
  }
33
33
  catch (error) {
34
34
  return resolve({
35
+ prevented: true,
35
36
  error: 'WAVE did not return JSON.',
36
37
  report
37
38
  });