testaro 5.6.0 → 5.6.3

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.
@@ -0,0 +1,2697 @@
1
+ /* eslint-disable semi */
2
+ /* eslint-disable no-unused-vars */
3
+ /* eslint-disable no-useless-escape */
4
+ /* eslint-disable brace-style */
5
+ /* eslint-disable no-case-declarations */
6
+ /* eslint-disable no-use-before-define */
7
+ /* eslint-disable quotes */
8
+ /* eslint-disable indent */
9
+ var LevelAccess_AccessContinuumVersion="community";'use strict';
10
+
11
+ // media type IDs as defined in AMP
12
+ const WEB_MEDIA_TYPE_ID = 1;
13
+
14
+ /**
15
+ * This class encapsulates all of the helper functionality Access Continuum offers for running Access Engine to test web projects.
16
+ *
17
+ * @hideconstructor
18
+ */
19
+ class Continuum {
20
+
21
+ /**
22
+ * @constructor
23
+ * @returns {Continuum}
24
+ */
25
+ constructor() {
26
+ this._accessEngineCode = null;
27
+ this._accessibilityConcerns = null;
28
+ this._includePotentialAccessibilityConcerns = null;
29
+
30
+ this._driver = null;
31
+ this._configPath = null;
32
+ this._windowUnderTest = null;
33
+
34
+ this._bestPracticeDataById = {};
35
+
36
+ this._webBestPracticeIds = [];
37
+ this._webTestNameById = {};
38
+ this._webBestPracticeNameById = {};
39
+ this._webStandardNameById = {};
40
+
41
+ this._AMPReportingService = null;
42
+ }
43
+
44
+ /**
45
+ * @private
46
+ * @returns {string}
47
+ */
48
+ get accessEngineCode() {
49
+ return this._accessEngineCode;
50
+ }
51
+
52
+ set accessEngineCode(accessEngineCode) {
53
+ this._accessEngineCode = accessEngineCode;
54
+ }
55
+
56
+ /**
57
+ * @private
58
+ * @returns {AccessibilityConcern[]}
59
+ */
60
+ get accessibilityConcerns() {
61
+ return this._accessibilityConcerns;
62
+ }
63
+
64
+ set accessibilityConcerns(accessibilityConcerns) {
65
+ this._accessibilityConcerns = accessibilityConcerns;
66
+ }
67
+
68
+ /**
69
+ * Defines whether or not accessibility concerns that require manual review are included in any of Continuum's test results.
70
+ * This functionality is disabled by default, but it can be enabled via {@link Continuum#setIncludePotentialAccessibilityConcerns}.
71
+ * If enabled, any accessibility concerns that require manual review will have {@link AccessibilityConcern#needsReview} return true.
72
+ *
73
+ * @returns {boolean}
74
+ */
75
+ get includePotentialAccessibilityConcerns() {
76
+ return this._includePotentialAccessibilityConcerns;
77
+ }
78
+
79
+ /**
80
+ * Globally sets whether or not accessibility concerns that require manual review are included in any of Continuum's test results.
81
+ * If enabled, any accessibility concerns that require manual review will have {@link AccessibilityConcern#needsReview} return true.
82
+ *
83
+ * This method is only available in the Pro edition of Continuum, otherwise it will return a Promise that rejects immediately.
84
+ *
85
+ * @param {boolean} includePotentialAccessibilityConcerns - whether or not accessibility concerns that require manual review should be returned in any of Continuum's test results
86
+ * @returns {Promise}
87
+ */
88
+ async setIncludePotentialAccessibilityConcerns(includePotentialAccessibilityConcerns) {
89
+ if (LevelAccess_AccessContinuumVersion !== "professional") {
90
+ if (includePotentialAccessibilityConcerns) {
91
+ console.log("setIncludePotentialAccessibilityConcerns() is not available in the Community edition of Continuum. Please upgrade to the Pro edition of Continuum for access to this method.");
92
+ }
93
+ includePotentialAccessibilityConcerns = false;
94
+ }
95
+
96
+ this._includePotentialAccessibilityConcerns = includePotentialAccessibilityConcerns;
97
+
98
+ // Continuum needs to be reinitialized to properly propagate the changes above
99
+ await this.setUp(this.driver, this.configPath, this.windowUnderTest);
100
+ return true;
101
+ }
102
+
103
+ /**
104
+ * @private
105
+ * @returns {*}
106
+ */
107
+ get driver() {
108
+ return this._driver;
109
+ }
110
+
111
+ set driver(driver) {
112
+ this._driver = driver;
113
+
114
+ if (this.AMPReportingService) {
115
+ this.AMPReportingService.driver = driver;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * @private
121
+ * @returns {string}
122
+ */
123
+ get configPath() {
124
+ return this._configPath;
125
+ }
126
+
127
+ set configPath(configPath) {
128
+ this._configPath = configPath;
129
+ }
130
+
131
+ /**
132
+ * @private
133
+ * @returns {Window}
134
+ */
135
+ get windowUnderTest() {
136
+ return this._windowUnderTest;
137
+ }
138
+
139
+ set windowUnderTest(window) {
140
+ this._windowUnderTest = window;
141
+
142
+ if (this.AMPReportingService) {
143
+ this.AMPReportingService.windowUnderTest = window;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * @private
149
+ * @returns {object}
150
+ */
151
+ get bestPracticeDataById() {
152
+ return this._bestPracticeDataById;
153
+ }
154
+
155
+ set bestPracticeDataById(bestPracticeDataById) {
156
+ this._bestPracticeDataById = bestPracticeDataById;
157
+ }
158
+
159
+ /**
160
+ * @private
161
+ * @returns {number[]}
162
+ */
163
+ get webBestPracticeIds() {
164
+ return this._webBestPracticeIds;
165
+ }
166
+
167
+ set webBestPracticeIds(webBestPracticeIds) {
168
+ this._webBestPracticeIds = webBestPracticeIds;
169
+ }
170
+
171
+ /**
172
+ * @private
173
+ * @returns {object}
174
+ */
175
+ get webTestNameById() {
176
+ return this._webTestNameById;
177
+ }
178
+
179
+ set webTestNameById(webTestNameById) {
180
+ this._webTestNameById = webTestNameById;
181
+ }
182
+
183
+ /**
184
+ * @private
185
+ * @returns {object}
186
+ */
187
+ get webBestPracticeNameById() {
188
+ return this._webBestPracticeNameById;
189
+ }
190
+
191
+ set webBestPracticeNameById(webBestPracticeNameById) {
192
+ this._webBestPracticeNameById = webBestPracticeNameById;
193
+ }
194
+
195
+ /**
196
+ * @private
197
+ * @returns {object}
198
+ */
199
+ get webStandardNameById() {
200
+ return this._webStandardNameById;
201
+ }
202
+
203
+ set webStandardNameById(webStandardNameById) {
204
+ this._webStandardNameById = webStandardNameById;
205
+ }
206
+
207
+ /**
208
+ * Gets the instance of the AMP reporting service associated with this instance of Continuum.
209
+ * Please consult our support documentation for more information on how to report to AMP.
210
+ *
211
+ * @returns {AMPReportingService} the AMP reporting service associated with this instance of Continuum
212
+ */
213
+ get AMPReportingService() {
214
+ return this._AMPReportingService;
215
+ }
216
+
217
+ set AMPReportingService(AMPReportingService) {
218
+ this._AMPReportingService = AMPReportingService;
219
+ }
220
+
221
+ /**
222
+ * Retrieves the Access Engine file contents from a local directory
223
+ *
224
+ * @private
225
+ */
226
+ _retrieveAccessEngineCode() {
227
+ switch (PlatformUtil.getRuntimeName()) {
228
+ case "Node":
229
+ const filePath = `${__dirname}/AccessEngine.${LevelAccess_AccessContinuumVersion}.js`;
230
+ const fileContent = require('fs').readFileSync(filePath, 'utf8');
231
+ this.accessEngineCode = this.createInjectableAccessEngineCode(fileContent);
232
+ return true;
233
+ default:
234
+ // we assume Access Engine was already injected by something externally if we're not able to inject it from here
235
+ return false;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Creates injectable Access Engine code from the specified Access Engine code to allow it to be injected into a page and used.
241
+ * This method can be used to inject Access Engine into the page yourself rather than having Continuum do it for you.
242
+ *
243
+ * @param {string} accessEngineCode - Access Engine JavaScript code
244
+ * @returns {string}
245
+ */
246
+ createInjectableAccessEngineCode(accessEngineCode) {
247
+ accessEngineCode += "window.LevelAccess_Continuum_AccessEngine = LevelAccess_AccessEngine;";
248
+ return accessEngineCode;
249
+ }
250
+
251
+ /**
252
+ * Injects Access Engine JavaScript code into the page currently under test, if necessary; if it's already injected, we do nothing.
253
+ * In a client-side JavaScript context, e.g. Karma, this function does nothing; it is assumed Access Engine has already been injected into the page through some other means.
254
+ *
255
+ * @private
256
+ */
257
+ async _injectAccessEngine() {
258
+ if (!this.accessEngineCode) {
259
+ this._retrieveAccessEngineCode();
260
+ }
261
+
262
+ if (this.accessEngineCode) {
263
+ if (this.driver) {
264
+ await this.driver.executeScript(this.accessEngineCode);
265
+ } else if (this.windowUnderTest) {
266
+ const hasEngineAlreadyBeenInjected = !!this.windowUnderTest.LevelAccess_Continuum_AccessEngine;
267
+ if (!hasEngineAlreadyBeenInjected) {
268
+ this.windowUnderTest.eval(this.accessEngineCode);
269
+ }
270
+ }
271
+ } else {
272
+ if (this.windowUnderTest) {
273
+ const hasEngineAlreadyBeenInjected = !!this.windowUnderTest.LevelAccess_Continuum_AccessEngine;
274
+ if (!hasEngineAlreadyBeenInjected) {
275
+ // assume Access Engine has been injected under the default namespace by something external to Continuum,
276
+ // in which case we just need to reassign it to a different namespace
277
+ this.windowUnderTest.LevelAccess_Continuum_AccessEngine = this.windowUnderTest.LevelAccess_AccessEngine;
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Gets test info from Access Engine as a JSON object whose keys are test IDs and values are metadata for the given test.
285
+ *
286
+ * @private
287
+ * @returns {object}
288
+ */
289
+ async _getTestInfo() {
290
+ await this._injectAccessEngine();
291
+
292
+ let data;
293
+ if (this.driver) {
294
+ const testTypeJsonArrayString = this.includePotentialAccessibilityConcerns ? "[4,5]" : "[4]";
295
+ data = await this.driver.executeScript(`return LevelAccess_Continuum_AccessEngine.getTestInfo({testType:${testTypeJsonArrayString},columns:[\"description\",\"bestPractice\",\"mediaType\"]});`);
296
+ return data;
297
+ } else if (this.windowUnderTest) {
298
+ const testTypeJsonArray = this.includePotentialAccessibilityConcerns ? [4,5] : [4];
299
+ data = this.windowUnderTest.LevelAccess_Continuum_AccessEngine.getTestInfo({
300
+ testType: testTypeJsonArray,
301
+ columns: ["description", "bestPractice", "mediaType"]
302
+ });
303
+ return data;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Attempts to fetch best practice data from the AMP instance specified by 'ampInstanceUrl' in continuum.conf.js.
309
+ * If this data cannot be fetched from AMP within a timeout period of 10 seconds, this method fails gracefully, outputting any errors to the console.
310
+ *
311
+ * @private
312
+ * @returns {object}
313
+ */
314
+ async _fetchBestPracticeData() {
315
+ const bestPracticeData = await NetworkUtil.getFromAMP('/api/cont/bestpractices', null, false, this.driver, this.windowUnderTest);
316
+
317
+ for (let i = 0; i < bestPracticeData.length; i++) {
318
+ const data = bestPracticeData[i];
319
+
320
+ const bestPracticeId = parseInt(data.bestPracticeID, 10);
321
+ if (bestPracticeId == null) {
322
+ continue;
323
+ }
324
+
325
+ const bestPracticeName = data.name;
326
+
327
+ this.bestPracticeDataById[bestPracticeId] = data;
328
+
329
+ if (this.webBestPracticeIds.includes(bestPracticeId)) {
330
+ this.webBestPracticeNameById[bestPracticeId] = bestPracticeName;
331
+ }
332
+
333
+ if (data.standards) {
334
+ const standards = [];
335
+ Object.keys(data.standards).forEach((standardIdString) => {
336
+ if (standardIdString && data.standards[standardIdString]) {
337
+ const standardId = parseInt(standardIdString, 10);
338
+ const standardName = data.standards[standardIdString].trim();
339
+
340
+ if (!Configuration.getDefaultStandardIds() || Configuration.getDefaultStandardIds().includes(standardId)) {
341
+ standards.push({
342
+ id: standardId,
343
+ name: standardName
344
+ });
345
+ }
346
+
347
+ if (this.webBestPracticeIds.includes(bestPracticeId)) {
348
+ this.webStandardNameById[standardId] = standardName;
349
+ }
350
+ }
351
+ });
352
+ standards.sort((a, b) => a.name.localeCompare(b.name));
353
+ data.standards = standards;
354
+ }
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Converts a raw JSON string of test results from Access Engine to an array of accessibility concerns.
360
+ * Also filters and enriches those accessibility concerns with best practice data from AMP, if available.
361
+ *
362
+ * @private
363
+ * @param {string} results - a raw JSON string of test results from Access Engine
364
+ * @returns {AccessibilityConcern[]}
365
+ */
366
+ _convertAccessEngineResultsToAccessibilityConcerns(results) {
367
+ if (!results) {
368
+ return null;
369
+ }
370
+
371
+ const accessibilityConcerns = [];
372
+
373
+ const resultsJson = JSON.parse(results);
374
+ const haveBestPracticeData = (Object.keys(this.bestPracticeDataById).length > 0);
375
+ for (let i = 0; i < resultsJson.length; i++) {
376
+ const result = resultsJson[i];
377
+ const transformedResult = {};
378
+
379
+ transformedResult.bestPracticeId = parseInt(result.bestPracticeId, 10) || null;
380
+ transformedResult.engineTestId = parseInt(result.engineTestId, 10) || null;
381
+
382
+ transformedResult.needsReview = (result.testResult === 3);
383
+
384
+ // only filter and enrich test results if a connection to the specified AMP instance could be established
385
+ if (haveBestPracticeData) {
386
+ const bestPracticeData = this.bestPracticeDataById[transformedResult.bestPracticeId];
387
+ if (!bestPracticeData) {
388
+ continue;
389
+ }
390
+
391
+ if (bestPracticeData.standards.length <= 0) {
392
+ // don't surface accessibility concerns with no relevant accessibility standards
393
+ continue;
394
+ }
395
+
396
+ transformedResult.bestPracticeDescription = bestPracticeData.name;
397
+ transformedResult.severity = parseInt(bestPracticeData.severity, 10) || null;
398
+ transformedResult.noticeability = parseInt(bestPracticeData.noticeability, 10) || null;
399
+ transformedResult.tractability = parseInt(bestPracticeData.tractability, 10) || null;
400
+ transformedResult.bestPracticeDetailsUrl = bestPracticeData.href;
401
+ transformedResult.bestPracticeStandards = bestPracticeData.standards;
402
+ }
403
+
404
+ const accessibilityConcern = new AccessibilityConcern(
405
+ result.path, transformedResult.engineTestId, result.attributeDetail,
406
+ transformedResult.bestPracticeId, result.element, result.fixType,
407
+ transformedResult.needsReview, result, transformedResult.bestPracticeDescription,
408
+ transformedResult.severity, transformedResult.noticeability, transformedResult.tractability,
409
+ transformedResult.bestPracticeDetailsUrl, transformedResult.bestPracticeStandards);
410
+ accessibilityConcerns.push(accessibilityConcern);
411
+ }
412
+
413
+ return accessibilityConcerns;
414
+ }
415
+
416
+ /**
417
+ * Runs only the automatic Access Engine tests corresponding to the specified accessibility standards against the current page for only the specified node and all its children.
418
+ * Note that the IDs of the specified accessibility standards must also be specified by {@link Configuration#getAccessEngineType}, otherwise no accessibility concerns will be returned.
419
+ *
420
+ * @private
421
+ * @param {number[]} standardIds - the IDs of the accessibility standards to test for (invoke {@link Continuum#getSupportedStandards} for a list of these)
422
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
423
+ * @returns {Promise}
424
+ */
425
+ _testForStandardsImpl(standardIds, targetNodeOrCssSelectorForTargetNode) {
426
+ const filterResults = (results) => {
427
+ const filteredAccessibilityConcerns = [];
428
+ if (standardIds != null && results != null) {
429
+ results.forEach((result) => {
430
+ const bestPracticeStandardIds = result.bestPracticeStandards ? result.bestPracticeStandards.map(x => x.id) : [];
431
+ if (bestPracticeStandardIds.some(x => standardIds.includes(x))) {
432
+ filteredAccessibilityConcerns.push(result);
433
+ }
434
+ });
435
+ }
436
+ return filteredAccessibilityConcerns;
437
+ };
438
+
439
+ return new Promise((resolve, reject) => {
440
+ if (targetNodeOrCssSelectorForTargetNode == null) {
441
+ this.runAllTests().then((results) => {
442
+ this.accessibilityConcerns = filterResults(results);
443
+ resolve(this.accessibilityConcerns);
444
+ });
445
+ } else {
446
+ this.runAllTestsOnNode(targetNodeOrCssSelectorForTargetNode).then((results) => {
447
+ this.accessibilityConcerns = filterResults(results);
448
+ resolve(this.accessibilityConcerns);
449
+ });
450
+ }
451
+ });
452
+ }
453
+
454
+ /**
455
+ * Runs only the automatic Access Engine tests corresponding to the specified best practices against the current page for only the specified node and all its children.
456
+ *
457
+ * @private
458
+ * @param {number[]} bestPracticeIds - the IDs of the best practices to test for (invoke {@link Continuum#getSupportedBestPractices} for a list of these, or consult AMP)
459
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
460
+ * @returns {Promise}
461
+ */
462
+ _testForBestPracticesImpl(bestPracticeIds, targetNodeOrCssSelectorForTargetNode) {
463
+ const filterResults = (results) => {
464
+ const filteredAccessibilityConcerns = [];
465
+ if (bestPracticeIds != null && results != null) {
466
+ results.forEach((result) => {
467
+ if (bestPracticeIds.includes(result.bestPracticeId)) {
468
+ filteredAccessibilityConcerns.push(result);
469
+ }
470
+ });
471
+ }
472
+ return filteredAccessibilityConcerns;
473
+ };
474
+
475
+ return new Promise((resolve, reject) => {
476
+ if (targetNodeOrCssSelectorForTargetNode == null) {
477
+ this.runAllTests().then((results) => {
478
+ this.accessibilityConcerns = filterResults(results);
479
+ resolve(this.accessibilityConcerns);
480
+ });
481
+ } else {
482
+ this.runAllTestsOnNode(targetNodeOrCssSelectorForTargetNode).then((results) => {
483
+ this.accessibilityConcerns = filterResults(results);
484
+ resolve(this.accessibilityConcerns);
485
+ });
486
+ }
487
+ });
488
+ }
489
+
490
+ /**
491
+ * Runs only the specified Access Engine tests against the current page for only the specified node and all its children.
492
+ *
493
+ * @private
494
+ * @param {number[]} accessEngineTestIds - the IDs of the automatic Access Engine tests to test for (invoke {@link Continuum#getSupportedTests} for a list of these, or consult AMP)
495
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
496
+ * @returns {Promise}
497
+ */
498
+ _runTestsImpl(accessEngineTestIds, targetNodeOrCssSelectorForTargetNode) {
499
+ const filterResults = (results) => {
500
+ const filteredAccessibilityConcerns = [];
501
+ if (accessEngineTestIds != null && results != null) {
502
+ results.forEach((result) => {
503
+ if (accessEngineTestIds.includes(result.engineTestId)) {
504
+ filteredAccessibilityConcerns.push(result);
505
+ }
506
+ });
507
+ }
508
+ return filteredAccessibilityConcerns;
509
+ };
510
+
511
+ return new Promise((resolve, reject) => {
512
+ if (targetNodeOrCssSelectorForTargetNode == null) {
513
+ this.runAllTests().then((results) => {
514
+ this.accessibilityConcerns = filterResults(results);
515
+ resolve(this.accessibilityConcerns);
516
+ });
517
+ } else {
518
+ this.runAllTestsOnNode(targetNodeOrCssSelectorForTargetNode).then((results) => {
519
+ this.accessibilityConcerns = filterResults(results);
520
+ resolve(this.accessibilityConcerns);
521
+ });
522
+ }
523
+ });
524
+ }
525
+
526
+ /**
527
+ * Runs only the automatic Access Engine tests of or greater than the specified severity against the current page for only the specified node and all its children.
528
+ *
529
+ * @private
530
+ * @param {number} minSeverity - the inclusive minimum severity of accessibility concerns to test for on a scale of 1 to 10, where 1 is the least severe and 10 is the most severe
531
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
532
+ * @returns {Promise}
533
+ */
534
+ _testForSeverityImpl(minSeverity, targetNodeOrCssSelectorForTargetNode) {
535
+ const filterResults = (results) => {
536
+ const filteredAccessibilityConcerns = [];
537
+ if (minSeverity != null && results != null) {
538
+ results.forEach((result) => {
539
+ if (result.severity && result.severity >= minSeverity) {
540
+ filteredAccessibilityConcerns.push(result);
541
+ }
542
+ });
543
+ }
544
+ return filteredAccessibilityConcerns;
545
+ };
546
+
547
+ return new Promise((resolve, reject) => {
548
+ if (targetNodeOrCssSelectorForTargetNode == null) {
549
+ this.runAllTests().then((results) => {
550
+ this.accessibilityConcerns = filterResults(results);
551
+ resolve(this.accessibilityConcerns);
552
+ });
553
+ } else {
554
+ this.runAllTestsOnNode(targetNodeOrCssSelectorForTargetNode).then((results) => {
555
+ this.accessibilityConcerns = filterResults(results);
556
+ resolve(this.accessibilityConcerns);
557
+ });
558
+ }
559
+ });
560
+ }
561
+
562
+ /**
563
+ * Runs only the automatic Access Engine tests of or greater than the specified tractability against the current page for only the specified node and all its children.
564
+ *
565
+ * @private
566
+ * @param {number} minTractability - the inclusive minimum tractability of accessibility concerns to test for on a scale of 1 to 10, where 1 is the least tractable and 10 is the most tractable
567
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
568
+ * @returns {Promise}
569
+ */
570
+ _testForTractabilityImpl(minTractability, targetNodeOrCssSelectorForTargetNode) {
571
+ const filterResults = (results) => {
572
+ const filteredAccessibilityConcerns = [];
573
+ if (minTractability != null && results != null) {
574
+ results.forEach((result) => {
575
+ if (result.tractability && result.tractability >= minTractability) {
576
+ filteredAccessibilityConcerns.push(result);
577
+ }
578
+ });
579
+ }
580
+ return filteredAccessibilityConcerns;
581
+ };
582
+
583
+ return new Promise((resolve, reject) => {
584
+ if (targetNodeOrCssSelectorForTargetNode == null) {
585
+ this.runAllTests().then((results) => {
586
+ this.accessibilityConcerns = filterResults(results);
587
+ resolve(this.accessibilityConcerns);
588
+ });
589
+ } else {
590
+ this.runAllTestsOnNode(targetNodeOrCssSelectorForTargetNode).then((results) => {
591
+ this.accessibilityConcerns = filterResults(results);
592
+ resolve(this.accessibilityConcerns);
593
+ });
594
+ }
595
+ });
596
+ }
597
+
598
+ /**
599
+ * Runs only the automatic Access Engine tests of or greater than the specified noticeability against the current page for only the specified node and all its children.
600
+ *
601
+ * @private
602
+ * @param {number} minNoticeability - the inclusive minimum noticeability of accessibility concerns to test for on a scale of 1 to 10, where 1 is the least noticeable and 10 is the most noticeable
603
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
604
+ * @returns {Promise}
605
+ */
606
+ _testForNoticeabilityImpl(minNoticeability, targetNodeOrCssSelectorForTargetNode) {
607
+ const filterResults = (results) => {
608
+ const filteredAccessibilityConcerns = [];
609
+ if (minNoticeability != null && results != null) {
610
+ results.forEach((result) => {
611
+ if (result.noticeability && result.noticeability >= minNoticeability) {
612
+ filteredAccessibilityConcerns.push(result);
613
+ }
614
+ });
615
+ }
616
+ return filteredAccessibilityConcerns;
617
+ };
618
+
619
+ return new Promise((resolve, reject) => {
620
+ if (targetNodeOrCssSelectorForTargetNode == null) {
621
+ this.runAllTests().then((results) => {
622
+ this.accessibilityConcerns = filterResults(results);
623
+ resolve(this.accessibilityConcerns);
624
+ });
625
+ } else {
626
+ this.runAllTestsOnNode(targetNodeOrCssSelectorForTargetNode).then((results) => {
627
+ this.accessibilityConcerns = filterResults(results);
628
+ resolve(this.accessibilityConcerns);
629
+ });
630
+ }
631
+ });
632
+ }
633
+
634
+ /////
635
+ // API Functions
636
+
637
+ /**
638
+ * Sets up Continuum for web testing.
639
+ * Either the webDriver or window parameter must be specified here.
640
+ *
641
+ * @param {?*} webDriver - a Selenium web driver to test with
642
+ * @param {?string} configPath - the absolute path to a valid continuum.conf.js file; null if you've already loaded this yourself
643
+ * @param {?Window} window - the window whose content should be tested
644
+ */
645
+ async setUp(webDriver, configPath, window) {
646
+ this.driver = webDriver;
647
+ this.configPath = configPath;
648
+ this.windowUnderTest = window;
649
+
650
+ Configuration.load(configPath);
651
+
652
+ if (this._includePotentialAccessibilityConcerns === null) {
653
+ if (await this.setIncludePotentialAccessibilityConcerns(Configuration.getIncludePotentialAccessibilityConcerns())) {
654
+ // on success, setIncludePotentialAccessibilityConcerns calls setUp, so no need to finish executing setUp here
655
+ return;
656
+ }
657
+ }
658
+ if (this._AMPReportingService === null) {
659
+ this._AMPReportingService = new AMPReportingService(this._driver, this._windowUnderTest);
660
+ }
661
+
662
+ this._retrieveAccessEngineCode();
663
+
664
+ let testDataFetched = false;
665
+ try {
666
+ // inject Engine and fetch info about its automatic tests
667
+ const testInfo = await this._getTestInfo();
668
+
669
+ // parse and bucket test info by platform
670
+ if (testInfo != null) {
671
+ Object.keys(testInfo).forEach((testIdString) => {
672
+ const testId = parseInt(testIdString, 10);
673
+ const testInfoData = testInfo[testId];
674
+
675
+ if (testInfoData.mediaType === 1) {
676
+ const bestPracticeId = parseInt(testInfoData.bestPractice, 10);
677
+ this.webBestPracticeIds.push(bestPracticeId);
678
+ this.webTestNameById[testId] = testInfoData.description;
679
+ }
680
+ });
681
+
682
+ testDataFetched = true;
683
+ }
684
+ } catch (err) {
685
+ console.log(err);
686
+ } finally {
687
+ if (!testDataFetched) {
688
+ console.log("Failed to fetch info about tests supported by Access Engine! Continuum is now operating in a degraded state; getSupportedTests(), getSupportedBestPractices(), and getSupportedStandards() will not return any data.");
689
+ }
690
+ }
691
+
692
+ try {
693
+ // prefetch best practice data from AMP
694
+ await this._fetchBestPracticeData();
695
+ } catch (err) {
696
+ console.log("Failed to fetch enriched best practice data from AMP! Continuum is now operating in a degraded state; both getSupportedBestPractices() and getSupportedStandards() will not return any data, and accessibility concerns returned by Continuum will not be enriched with corresponding best practice data from AMP.", err);
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Sets the window to test.
702
+ * This can be used to set the testing context to the contents of an iframe element on the page, rather than the page an iframe element appears on.
703
+ *
704
+ * @param {Window} targetWindow - the window to inject Access Engine into and prepare to test
705
+ * @returns {Promise}
706
+ */
707
+ setWindowUnderTest(targetWindow) {
708
+ return new Promise((resolve, reject) => {
709
+ const injectAccessEngine = this._injectAccessEngine();
710
+
711
+ const execApi = new Promise((resolve, reject) => {
712
+ if (this.driver) {
713
+ // not supported
714
+ reject();
715
+ } else if (this.windowUnderTest) {
716
+ this.windowUnderTest.LevelAccess_Continuum_AccessEngine.setWindowUnderTest(targetWindow);
717
+ resolve();
718
+ } else {
719
+ reject();
720
+ }
721
+ });
722
+
723
+ const arr = [injectAccessEngine, execApi];
724
+ Promise.all(arr).then(() => {
725
+ resolve();
726
+ });
727
+ });
728
+ }
729
+
730
+ /**
731
+ * Runs all automatic Access Engine tests against the current page, as defined by the web driver used previously to invoke {@link Continuum#setUp}.
732
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
733
+ *
734
+ * @returns {Promise}
735
+ */
736
+ runAllTests() {
737
+ return new Promise((resolve, reject) => {
738
+ const injectAccessEngine = this._injectAccessEngine();
739
+
740
+ const execApi = new Promise((resolve, reject) => {
741
+ if (this.driver) {
742
+ const testTypeJsonArrayString = this.includePotentialAccessibilityConcerns ? "[4,5]" : "[4]";
743
+ this.driver.executeScript(`return LevelAccess_Continuum_AccessEngine.ast_runAllTests_returnInstances_JSON(${testTypeJsonArrayString});`).then((outcome) => {
744
+ this.accessibilityConcerns = this._convertAccessEngineResultsToAccessibilityConcerns(outcome);
745
+ resolve(this.accessibilityConcerns);
746
+ });
747
+ } else if (this.windowUnderTest) {
748
+ const testTypeJsonArray = this.includePotentialAccessibilityConcerns ? [4,5] : [4];
749
+ this.accessibilityConcerns = this._convertAccessEngineResultsToAccessibilityConcerns(this.windowUnderTest.LevelAccess_Continuum_AccessEngine.ast_runAllTests_returnInstances_JSON(testTypeJsonArray));
750
+ resolve(this.accessibilityConcerns);
751
+ } else {
752
+ reject();
753
+ }
754
+ });
755
+
756
+ const arr = [injectAccessEngine, execApi];
757
+ Promise.all(arr).then(() => {
758
+ resolve(this.accessibilityConcerns);
759
+ });
760
+ });
761
+ }
762
+
763
+ /**
764
+ * Runs only the automatic Access Engine tests corresponding to the specified accessibility standards against the current page, as defined by the web driver used previously to invoke {@link Continuum#setUp}.
765
+ * Note that the IDs of the specified accessibility standards must also be specified by {@link Configuration#getAccessEngineType}, otherwise no accessibility concerns will be returned.
766
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
767
+ *
768
+ * @param {number[]} standardIds - the IDs of the accessibility standards to test for (invoke {@link Continuum#getSupportedStandards} for a list of these, or consult AMP)
769
+ * @returns {Promise}
770
+ */
771
+ testForStandards(standardIds) {
772
+ return this._testForStandardsImpl(standardIds, null);
773
+ }
774
+
775
+ /**
776
+ * Runs only the automatic Access Engine tests corresponding to the specified best practices against the current page, as defined by the web driver used previously to invoke {@link Continuum#setUp}.
777
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
778
+ *
779
+ * @param {number[]} bestPracticeIds - the IDs of the best practices to test for (invoke {@link Continuum#getSupportedBestPractices} for a list of these, or consult AMP)
780
+ * @returns {Promise}
781
+ */
782
+ testForBestPractices(bestPracticeIds) {
783
+ return this._testForBestPracticesImpl(bestPracticeIds, null);
784
+ }
785
+
786
+ /**
787
+ * Runs only the specified automatic Access Engine tests against the current page, as defined by the web driver used previously to invoke {@link Continuum#setUp}.
788
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
789
+ *
790
+ * @param {number[]} engineTestIds - the IDs of the automatic Access Engine tests to test for (invoke {@link Continuum#getSupportedTests} for a list of these, or consult AMP)
791
+ * @returns {Promise}
792
+ */
793
+ runTests(engineTestIds) {
794
+ return this._runTestsImpl(engineTestIds, null);
795
+ }
796
+
797
+ /**
798
+ * Runs only the automatic Access Engine tests of or greater than the specified severity against the current page, as defined by the web driver used previously to invoke {@link Continuum#setUp}.
799
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
800
+ *
801
+ * @param {number} minSeverity - the inclusive minimum severity of accessibility concerns to test for on a scale of 1 to 10, where 1 is the least severe and 10 is the most severe
802
+ * @returns {Promise}
803
+ */
804
+ testForSeverity(minSeverity) {
805
+ return this._testForSeverityImpl(minSeverity, null);
806
+ }
807
+
808
+ /**
809
+ * Runs only the automatic Access Engine tests of or greater than the specified tractability against the current page, as defined by the web driver used previously to invoke {@link Continuum#setUp}.
810
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
811
+ *
812
+ * @param {number} minTractability - the inclusive minimum tractability of accessibility concerns to test for on a scale of 1 to 10, where 1 is the least tractable and 10 is the most tractable
813
+ * @returns {Promise}
814
+ */
815
+ testForTractability(minTractability) {
816
+ return this._testForTractabilityImpl(minTractability, null);
817
+ }
818
+
819
+ /**
820
+ * Runs only the automatic Access Engine tests of or greater than the specified noticeability against the current page, as defined by the web driver used previously to invoke {@link Continuum#setUp}.
821
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
822
+ *
823
+ * @param {number} minNoticeability - the inclusive minimum noticeability of accessibility concerns to test for on a scale of 1 to 10, where 1 is the least noticeable and 10 is the most noticeable
824
+ * @returns {Promise}
825
+ */
826
+ testForNoticeability(minNoticeability) {
827
+ return this._testForNoticeabilityImpl(minNoticeability, null);
828
+ }
829
+
830
+ /**
831
+ * Runs all automatic Access Engine tests against the current page for only the specified node and all its children, as defined by the web driver used previously to invoke {@link Continuum#setUp} and the specified node or its CSS selector.
832
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
833
+ *
834
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
835
+ * @returns {Promise}
836
+ */
837
+ runAllTestsOnNode(targetNodeOrCssSelectorForTargetNode) {
838
+ return new Promise((resolve, reject) => {
839
+ const injectAccessEngine = this._injectAccessEngine();
840
+
841
+ const execApi = new Promise((resolve, reject) => {
842
+ if (this.driver) {
843
+ if (typeof targetNodeOrCssSelectorForTargetNode === 'string' || targetNodeOrCssSelectorForTargetNode instanceof String) {
844
+ const cssSelectorForTargetNode = targetNodeOrCssSelectorForTargetNode;
845
+ const script = this.includePotentialAccessibilityConcerns ? `return LevelAccess_Continuum_AccessEngine.ast_runAllTests_returnInstances_JSON_NodeCapture(document.querySelector("${cssSelectorForTargetNode}"),[4,5]);` : `return LevelAccess_Continuum_AccessEngine.runAllTests_returnInstances_JSON_NodeCapture(document.querySelector("${cssSelectorForTargetNode}"));`;
846
+
847
+ this.driver.executeScript(script).then((outcome) => {
848
+ this.accessibilityConcerns = this._convertAccessEngineResultsToAccessibilityConcerns(outcome);
849
+ resolve(this.accessibilityConcerns);
850
+ });
851
+ } else {
852
+ // not supported
853
+ reject();
854
+ }
855
+ } else if (this.windowUnderTest) {
856
+ let targetNode;
857
+ if (typeof targetNodeOrCssSelectorForTargetNode === 'string' || targetNodeOrCssSelectorForTargetNode instanceof String) {
858
+ const cssSelectorForTargetNode = targetNodeOrCssSelectorForTargetNode;
859
+ targetNode = this.windowUnderTest.document.querySelector(cssSelectorForTargetNode);
860
+ } else {
861
+ targetNode = targetNodeOrCssSelectorForTargetNode;
862
+ }
863
+
864
+ let results;
865
+ if (this.includePotentialAccessibilityConcerns) {
866
+ results = this.windowUnderTest.LevelAccess_Continuum_AccessEngine.ast_runAllTests_returnInstances_JSON_NodeCapture(targetNode, [4, 5]);
867
+ } else {
868
+ results = this.windowUnderTest.LevelAccess_Continuum_AccessEngine.runAllTests_returnInstances_JSON_NodeCapture(targetNode);
869
+ }
870
+ this.accessibilityConcerns = this._convertAccessEngineResultsToAccessibilityConcerns(results);
871
+
872
+ resolve(this.accessibilityConcerns);
873
+ } else {
874
+ reject();
875
+ }
876
+ });
877
+
878
+ const arr = [injectAccessEngine, execApi];
879
+ Promise.all(arr).then(() => {
880
+ resolve(this.accessibilityConcerns);
881
+ });
882
+ });
883
+ }
884
+
885
+ /**
886
+ * Runs only the automatic Access Engine tests corresponding to the specified accessibility standards against the current page for only the specified node and all its children, as defined by the web driver used previously to invoke {@link Continuum#setUp} and the specified node or its CSS selector.
887
+ * Note that the IDs of the specified accessibility standards must also be specified by {@link Configuration#getAccessEngineType}, otherwise no accessibility concerns will be returned.
888
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
889
+ *
890
+ * @param {number[]} standardIds - the IDs of the accessibility standards to test for (invoke {@link Continuum#getSupportedStandards} for a list of these, or consult AMP)
891
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
892
+ * @returns {Promise}
893
+ */
894
+ testNodeForStandards(standardIds, targetNodeOrCssSelectorForTargetNode) {
895
+ return this._testForStandardsImpl(standardIds, targetNodeOrCssSelectorForTargetNode);
896
+ }
897
+
898
+ /**
899
+ * Runs only the automatic Access Engine tests corresponding to the specified best practices against the current page for only the specified node and all its children, as defined by the web driver used previously to invoke {@link Continuum#setUp} and the specified node or its CSS selector.
900
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
901
+ *
902
+ * @param {number[]} bestPracticeIds - the IDs of the best practices to test for (invoke {@link Continuum#getSupportedBestPractices} for a list of these, or consult AMP)
903
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
904
+ * @returns {Promise}
905
+ */
906
+ testNodeForBestPractices(bestPracticeIds, targetNodeOrCssSelectorForTargetNode) {
907
+ return this._testForBestPracticesImpl(bestPracticeIds, targetNodeOrCssSelectorForTargetNode);
908
+ }
909
+
910
+ /**
911
+ * Runs only the specified automatic Access Engine tests against the current page for only the specified node and all its children, as defined by the web driver used previously to invoke {@link Continuum#setUp} and the specified node or its CSS selector.
912
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
913
+ *
914
+ * @param {number[]} engineTestIds - the IDs of the automatic Access Engine tests to test for (invoke {@link Continuum#getSupportedTests} for a list of these, or consult AMP)
915
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
916
+ * @returns {Promise}
917
+ */
918
+ runTestsOnNode(engineTestIds, targetNodeOrCssSelectorForTargetNode) {
919
+ return this._runTestsImpl(engineTestIds, targetNodeOrCssSelectorForTargetNode);
920
+ }
921
+
922
+ /**
923
+ * Runs only the automatic Access Engine tests of or greater than the specified severity against the current page for only the specified node and all its children, as defined by the web driver used previously to invoke {@link Continuum#setUp} and the specified node or its CSS selector.
924
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
925
+ *
926
+ * @param {number} minSeverity - the inclusive minimum severity of accessibility concerns to test for on a scale of 1 to 10, where 1 is the least noticeable and 10 is the most noticeable
927
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
928
+ * @returns {Promise}
929
+ */
930
+ testNodeForSeverity(minSeverity, targetNodeOrCssSelectorForTargetNode) {
931
+ return this._testForSeverityImpl(minSeverity, targetNodeOrCssSelectorForTargetNode);
932
+ }
933
+
934
+ /**
935
+ * Runs only the automatic Access Engine tests of or greater than the specified tractability against the current page for only the specified node and all its children, as defined by the web driver used previously to invoke {@link Continuum#setUp} and the specified node or its CSS selector.
936
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
937
+ *
938
+ * @param {number} minTractability - the inclusive minimum tractability of accessibility concerns to test for on a scale of 1 to 10, where 1 is the least noticeable and 10 is the most noticeable
939
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
940
+ * @returns {Promise}
941
+ */
942
+ testNodeForTractability(minTractability, targetNodeOrCssSelectorForTargetNode) {
943
+ return this._testForTractabilityImpl(minTractability, targetNodeOrCssSelectorForTargetNode);
944
+ }
945
+
946
+ /**
947
+ * Runs only the automatic Access Engine tests of or greater than the specified noticeability against the current page for only the specified node and all its children, as defined by the web driver used previously to invoke {@link Continuum#setUp} and the specified node or its CSS selector.
948
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
949
+ *
950
+ * @param {number} minNoticeability - the inclusive minimum noticeability of accessibility concerns to test for on a scale of 1 to 10, where 1 is the least noticeable and 10 is the most noticeable
951
+ * @param {(Element|string)} targetNodeOrCssSelectorForTargetNode - the target node, or its CSS selector, to restrict accessibility testing to
952
+ * @returns {Promise}
953
+ */
954
+ testNodeForNoticeability(minNoticeability, targetNodeOrCssSelectorForTargetNode) {
955
+ return this._testForNoticeabilityImpl(minNoticeability, targetNodeOrCssSelectorForTargetNode);
956
+ }
957
+
958
+ /**
959
+ * Gets an object of key-value pairs, where the keys are IDs of accessibility standards (defined in AMP and supported by Continuum) and the values are their names.
960
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
961
+ *
962
+ * @returns {object}
963
+ */
964
+ getSupportedStandards() {
965
+ return this.webStandardNameById;
966
+ }
967
+
968
+ /**
969
+ * Gets an object of key-value pairs, where the keys are IDs of best practices (defined in AMP and supported by Continuum) and the values are their descriptions.
970
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
971
+ *
972
+ * @returns {object}
973
+ */
974
+ getSupportedBestPractices() {
975
+ return this.webBestPracticeNameById;
976
+ }
977
+
978
+ /**
979
+ * Gets an object of key-value pairs, where the keys are IDs of automatic Access Engine tests (supported by Continuum) and the values are their descriptions.
980
+ * Make sure to invoke this {@link Continuum#setUp} method before invoking this method.
981
+ *
982
+ * @returns {object}
983
+ */
984
+ getSupportedTests() {
985
+ return this.webTestNameById;
986
+ }
987
+
988
+ /**
989
+ * Gets the list of accessibility concerns found by Access Engine during the last test execution.
990
+ *
991
+ * @returns {AccessibilityConcern[]}
992
+ */
993
+ getAccessibilityConcerns() {
994
+ return this.accessibilityConcerns;
995
+ }
996
+
997
+ /////
998
+ // Deprecated API Functions
999
+
1000
+ /**
1001
+ * @ignore
1002
+ * @deprecated Renamed for clarity; use {@link Continuum#runAllTests} instead.
1003
+ */
1004
+ runAllTests_returnInstances_JSON(callback) {
1005
+ return this.runAllTests();
1006
+ }
1007
+
1008
+ /**
1009
+ * @ignore
1010
+ * @deprecated Renamed for clarity; use {@link Continuum#runAllTestsOnNode} instead.
1011
+ */
1012
+ runAllTests_returnInstances_JSON_NodeCapture(targetNodeOrCssSelectorForTargetNode, callback) {
1013
+ return this.runAllTestsOnNode(targetNodeOrCssSelectorForTargetNode);
1014
+ }
1015
+
1016
+ /**
1017
+ * @ignore
1018
+ * @deprecated Renamed for clarity; use {@link Continuum#getAccessibilityConcerns} instead.
1019
+ */
1020
+ getA11yResults() {
1021
+ return this.getAccessibilityConcerns();
1022
+ }
1023
+ }
1024
+
1025
+ /**
1026
+ * This class encapsulates all Continuum configuration defined in the user-editable continuum.conf.js file and related functionality.
1027
+ *
1028
+ * @hideconstructor
1029
+ */
1030
+ class Configuration {
1031
+
1032
+ /**
1033
+ * @private
1034
+ */
1035
+ static get INSTANCE() {
1036
+ return Configuration.instance;
1037
+ }
1038
+
1039
+ /**
1040
+ * Reads the configuration defined in the continuum.conf.js file and writes it to a new Configuration object.
1041
+ * This method is not meant to be invoked directly outside the SDK internals; use {@link Continuum#setUp} instead.
1042
+ *
1043
+ * @ignore
1044
+ * @param {?string} configPath - the absolute path to a valid continuum.conf.js file; null if you've already loaded this yourself
1045
+ */
1046
+ static load(configPath) {
1047
+ let config;
1048
+
1049
+ switch (PlatformUtil.getRuntimeName()) {
1050
+ case "Node":
1051
+ const fileContents = require('fs').readFileSync(configPath, 'utf8');
1052
+ const modifiedFileContents = fileContents.replace("window.LevelAccess_AccessContinuumConfiguration = ", "return ");
1053
+ config = new Function(modifiedFileContents)();
1054
+ break;
1055
+ default:
1056
+ // we assume continuum.conf.js was already injected by something externally if we're not able to inject it from here
1057
+ config = window.LevelAccess_AccessContinuumConfiguration;
1058
+ break;
1059
+ }
1060
+
1061
+ const configuration = new Configuration();
1062
+
1063
+ configuration.accessEngineType = config.accessEngineType;
1064
+ configuration.ampInstanceUrl = config.ampInstanceUrl;
1065
+ configuration.defaultStandardIds = config.defaultStandardIds;
1066
+ configuration.includePotentialAccessibilityConcerns = config.includePotentialAccessibilityConcerns;
1067
+ configuration.ampApiToken = config.ampApiToken;
1068
+
1069
+ if (config.proxy) {
1070
+ configuration.proxyConfiguration = new Configuration.Proxy();
1071
+ configuration.proxyConfiguration.host = config.proxy.host;
1072
+ configuration.proxyConfiguration.port = config.proxy.port;
1073
+ configuration.proxyConfiguration.username = config.proxy.username;
1074
+ configuration.proxyConfiguration.password = config.proxy.password;
1075
+ } else {
1076
+ configuration.proxyConfiguration = null;
1077
+ }
1078
+
1079
+ Configuration.instance = configuration;
1080
+ }
1081
+
1082
+ /**
1083
+ * Gets the value for the 'accessEngineType' attribute defined in continuum.conf.js.
1084
+ * Used to determine which version of Access Engine is included with this installation of Continuum and should be used.
1085
+ *
1086
+ * @returns {string}
1087
+ */
1088
+ static getAccessEngineType() {
1089
+ return Configuration.INSTANCE.accessEngineType;
1090
+ }
1091
+
1092
+ /**
1093
+ * Gets the value for the 'ampInstanceUrl' attribute defined in continuum.conf.js.
1094
+ * The URL to the desired AMP instance from which to pull best practice data from.
1095
+ *
1096
+ * @returns {string}
1097
+ */
1098
+ static getAmpInstanceUrl() {
1099
+ return Configuration.INSTANCE.ampInstanceUrl;
1100
+ }
1101
+
1102
+ /**
1103
+ * Gets the set of IDs implied from the value of the 'defaultStandardIds' attribute defined in continuum.conf.js as a comma-delimited array of IDs of the accessibility standards to test for by default (invoke {@link Continuum#getSupportedStandards} for a list of these).
1104
+ * Set the value of the 'defaultStandardIds' attribute in continuum.conf.js to null to not filter by any accessibility standards by default.
1105
+ *
1106
+ * @returns {?number[]}
1107
+ */
1108
+ static getDefaultStandardIds() {
1109
+ return Configuration.INSTANCE.defaultStandardIds;
1110
+ }
1111
+
1112
+ /**
1113
+ * Gets the value for the 'includePotentialAccessibilityConcerns' attribute defined in continuum.conf.js.
1114
+ * Used to determine whether or not accessibility concerns that require manual review are returned in any of Continuum's test results.
1115
+ * If enabled, any accessibility concerns that require manual review will have {@link AccessibilityConcern#needsReview} return true.
1116
+ * This setting can be toggled programmatically using {@link Continuum#setIncludePotentialAccessibilityConcerns}, overriding this value specified in continuum.conf.js.
1117
+ *
1118
+ * @returns {boolean}
1119
+ */
1120
+ static getIncludePotentialAccessibilityConcerns() {
1121
+ return Configuration.INSTANCE.includePotentialAccessibilityConcerns;
1122
+ }
1123
+
1124
+ /**
1125
+ * Gets the value for the 'ampApiToken' attribute defined in continuum.conf.js.
1126
+ * The AMP API token of the user to use to authenticate any requests to AMP that require authentication, e.g. creating/editing reports in AMP from Continuum as part of submitting test results from Continuum to AMP.
1127
+ * Set to null if you don't want to take advantage of this functionality.
1128
+ *
1129
+ * @returns {?string}
1130
+ */
1131
+ static getAmpApiToken() {
1132
+ return Configuration.INSTANCE.ampApiToken;
1133
+ }
1134
+
1135
+ /**
1136
+ * Gets the proxy-specific configuration in Continuum represented by the 'proxy' object defined in continuum.conf.js.
1137
+ *
1138
+ * @returns {Configuration.Proxy}
1139
+ */
1140
+ static getProxyConfiguration() {
1141
+ return Configuration.INSTANCE.proxyConfiguration;
1142
+ }
1143
+ }
1144
+
1145
+ /**
1146
+ * This class encapsulates all proxy-specific configuration in Continuum represented by the 'proxy' object defined in the user-editable continuum.conf.js file.
1147
+ *
1148
+ * @hideconstructor
1149
+ */
1150
+ Configuration.Proxy = class Proxy {
1151
+ /**
1152
+ * Gets the value for the 'host' attribute of the the 'proxy' object defined in continuum.conf.js.
1153
+ * The IP address or hostname of the desired proxy to route all network traffic from Continuum through.
1154
+ * Set to null if you don't want to use a proxy.
1155
+ *
1156
+ * @returns {?string}
1157
+ */
1158
+ getHost() {
1159
+ return Configuration.INSTANCE.proxyConfiguration.host;
1160
+ }
1161
+
1162
+ /**
1163
+ * Gets the value for the 'port' attribute of the the 'proxy' object defined in continuum.conf.js.
1164
+ * The port of the desired proxy to route all network traffic from Continuum through.
1165
+ * Set to null if you don't want to use a proxy.
1166
+ *
1167
+ * @returns {?number}
1168
+ */
1169
+ getPort() {
1170
+ return Configuration.INSTANCE.proxyConfiguration.port;
1171
+ }
1172
+
1173
+ /**
1174
+ * Gets the value for the 'username' attribute of the the 'proxy' object defined in continuum.conf.js.
1175
+ * The username for the desired proxy to route all network traffic from Continuum through.
1176
+ * Set to null if your proxy does not require a username, or if you don't want to use a proxy.
1177
+ *
1178
+ * @returns {?string}
1179
+ */
1180
+ getUsername() {
1181
+ return Configuration.INSTANCE.proxyConfiguration.username;
1182
+ }
1183
+
1184
+ /**
1185
+ * Gets the value for the 'password' attribute of the the 'proxy' object defined in continuum.conf.js.
1186
+ * The password for the desired proxy to route all network traffic from Continuum through.
1187
+ * Set to null if your proxy does not require a password, or if you don't want to use a proxy.
1188
+ *
1189
+ * @returns {?string}
1190
+ */
1191
+ getPassword() {
1192
+ return Configuration.INSTANCE.proxyConfiguration.password;
1193
+ }
1194
+ };
1195
+
1196
+ /**
1197
+ * This class represents an accessibility concern identified by Access Engine.
1198
+ * At minimum, it contains both information about the concern that was identified and well as where on the page the problem is located.
1199
+ * It may also include best practice data from AMP, e.g. how severe or noticeable the issue might be, along with an AMP URL that can be visited for more info.
1200
+ */
1201
+ class AccessibilityConcern {
1202
+
1203
+ /**
1204
+ * @constructor
1205
+ * @param {string} path - a CSS selector to the element with this accessibility concern
1206
+ * @param {number} engineTestId - the automatic Access Engine test ID that failed and produced this accessibility concern
1207
+ * @param {string} attribute - a brief human-readable description of this accessibility concern
1208
+ * @param {number} bestPracticeId - the best practice ID that corresponds to this accessibility concern
1209
+ * @param {string} element - the source code of the HTML node corresponding to this accessibility concern
1210
+ * @param {FixType} fixType - the remediation steps suggested by Access Engine for resolving this accessibility concern
1211
+ * @param {boolean} needsReview - whether or not this accessibility concern requires manual review
1212
+ * @param {object} rawEngineJsonObject - the raw JSON object from Access Engine that was originally used to build this accessibility concern
1213
+ * @param {string} bestPracticeDescription - the name of the best practice that corresponds to this accessibility concern
1214
+ * @param {number} severity - the severity of the best practice that corresponds to this accessibility concern
1215
+ * @param {number} noticeability - the noticeability of the best practice that corresponds to this accessibility concern
1216
+ * @param {number} tractability - the tractability of the best practice that corresponds to this accessibility concern
1217
+ * @param {string} bestPracticeDetailsUrl - the URL of the best practice page in AMP that corresponds to this accessibility concern
1218
+ * @param {Standard[]} bestPracticeStandards - the array of accessibility standards associated with the best practice that corresponds to this accessibility concern
1219
+ * @returns {AccessibilityConcern}
1220
+ */
1221
+ constructor(path, engineTestId, attribute, bestPracticeId, element, fixType, needsReview, rawEngineJsonObject, bestPracticeDescription, severity, noticeability, tractability, bestPracticeDetailsUrl, bestPracticeStandards) {
1222
+ this._path = path;
1223
+ this._engineTestId = engineTestId;
1224
+ this._attribute = attribute;
1225
+ this._bestPracticeId = bestPracticeId;
1226
+ this._element = element;
1227
+ this._fixType = (fixType != null && (fixType.domSpec != null || fixType.helperText != null)) ? new FixType(fixType.domSpec, fixType.helperText) : null;
1228
+
1229
+ // enriched data from Continuum (that may be derived from Engine)
1230
+ this._needsReview = needsReview;
1231
+ this._rawEngineJsonObject = rawEngineJsonObject;
1232
+
1233
+ // optional: enriched best practice data from AMP
1234
+ this._bestPracticeDescription = bestPracticeDescription || null;
1235
+ this._severity = severity || null;
1236
+ this._noticeability = noticeability || null;
1237
+ this._tractability = tractability || null;
1238
+ this._bestPracticeDetailsUrl = bestPracticeDetailsUrl || null;
1239
+ this._bestPracticeStandards = bestPracticeStandards || null;
1240
+
1241
+ this.toJSON = function() {
1242
+ const result = {};
1243
+ for (let x in this) {
1244
+ // exclude member variables that shouldn't be exposed for readability purposes
1245
+ if (x !== "_rawEngineJsonObject") {
1246
+ result[x] = this[x];
1247
+ }
1248
+ }
1249
+ return result;
1250
+ };
1251
+ }
1252
+
1253
+ /**
1254
+ * A CSS (for web) or XPath (for mobile) selector to the element with this accessibility concern.
1255
+ *
1256
+ * @returns {string} a CSS selector to the element with this accessibility concern
1257
+ */
1258
+ get path() {
1259
+ return this._path;
1260
+ }
1261
+
1262
+ set path(path) {
1263
+ this._path = path;
1264
+ }
1265
+
1266
+ /**
1267
+ * The automatic Access Engine test ID that failed and produced this accessibility concern.
1268
+ * Visit the URL returned by {@link AccessibilityConcern#bestPracticeDetailsUrl} for more information.
1269
+ *
1270
+ * @returns {number} the automatic Access Engine test ID that failed and produced this accessibility concern
1271
+ */
1272
+ get engineTestId() {
1273
+ return this._engineTestId;
1274
+ }
1275
+
1276
+ set engineTestId(engineTestId) {
1277
+ this._engineTestId = engineTestId;
1278
+ }
1279
+
1280
+ /**
1281
+ * A brief human-readable description of this accessibility concern.
1282
+ * Visit the URL returned by {@link AccessibilityConcern#bestPracticeDetailsUrl} for more information.
1283
+ *
1284
+ * @returns {string} a brief human-readable description of this accessibility concern
1285
+ */
1286
+ get attribute() {
1287
+ return this._attribute;
1288
+ }
1289
+
1290
+ set attribute(attribute) {
1291
+ this._attribute = attribute;
1292
+ }
1293
+
1294
+ /**
1295
+ * The best practice ID that corresponds to this accessibility concern.
1296
+ * Visit the URL returned by {@link AccessibilityConcern#bestPracticeDetailsUrl} for more information.
1297
+ *
1298
+ * @returns {number} the best practice ID that corresponds to this accessibility concern
1299
+ */
1300
+ get bestPracticeId() {
1301
+ return this._bestPracticeId;
1302
+ }
1303
+
1304
+ set bestPracticeId(bestPracticeId) {
1305
+ this._bestPracticeId = bestPracticeId;
1306
+ }
1307
+
1308
+ /**
1309
+ * The source code of the HTML node corresponding to this accessibility concern.
1310
+ *
1311
+ * @returns {string} the source code of the HTML node corresponding to this accessibility concern
1312
+ */
1313
+ get element() {
1314
+ return this._element;
1315
+ }
1316
+
1317
+ set element(element) {
1318
+ this._element = element;
1319
+ }
1320
+
1321
+ /**
1322
+ * The remediation steps suggested by Access Engine for resolving this accessibility concern.
1323
+ *
1324
+ * @returns {FixType} the remediation steps suggested by Access Engine for resolving this accessibility concern
1325
+ */
1326
+ get fixType() {
1327
+ return this._fixType;
1328
+ }
1329
+
1330
+ set fixType(fixType) {
1331
+ this._fixType = fixType;
1332
+ }
1333
+
1334
+ /**
1335
+ * Gets whether or not this accessibility concern requires manual review, i.e. whether the user should manually use AMP to determine whether or not this accessibility concern is actually a legitimate violation given the context of the offending element ({@link AccessibilityConcern#element}).
1336
+ * If this returns true, visit the URL returned by {@link AccessibilityConcern#bestPracticeDetailsUrl} for more information on how to manually validate the applicability of this accessibility concern relative to the offending element ({@link AccessibilityConcern#element}); it may be that this accessibility concern is not applicable given this context.
1337
+ * Accessibility concerns that require manual review will only ever be returned (and thus this function will only ever possibly return false for a given accessibility concern) if {@link Continuum#includePotentialAccessibilityConcerns} returns true.
1338
+ *
1339
+ * @returns {boolean} whether or not this accessibility concern requires manual review
1340
+ */
1341
+ get needsReview() {
1342
+ return this._needsReview;
1343
+ }
1344
+
1345
+ set needsReview(needsReview) {
1346
+ this._needsReview = needsReview;
1347
+ }
1348
+
1349
+ /**
1350
+ * The raw JSON object from Access Engine that was originally used to build this accessibility concern.
1351
+ *
1352
+ * @returns {object} the raw JSON object from Access Engine that was originally used to build this accessibility concern
1353
+ */
1354
+ get rawEngineJsonObject() {
1355
+ return this._rawEngineJsonObject;
1356
+ }
1357
+
1358
+ set rawEngineJsonObject(rawEngineJsonObject) {
1359
+ this._rawEngineJsonObject = rawEngineJsonObject;
1360
+ }
1361
+
1362
+ /**
1363
+ * The name of the best practice that corresponds to this accessibility concern.
1364
+ * Visit the URL returned by {@link AccessibilityConcern#bestPracticeDetailsUrl} for more information.
1365
+ *
1366
+ * @returns {string} the name of the best practice that corresponds to this accessibility concern
1367
+ */
1368
+ get bestPracticeDescription() {
1369
+ return this._bestPracticeDescription;
1370
+ }
1371
+
1372
+ set bestPracticeDescription(bestPracticeDescription) {
1373
+ this._bestPracticeDescription = bestPracticeDescription;
1374
+ }
1375
+
1376
+ /**
1377
+ * The severity of this accessibility concern on a scale of 1 to 10, where 1 is the least severe and 10 is the most severe.
1378
+ * Visit the URL returned by {@link AccessibilityConcern#bestPracticeDetailsUrl} for more information.
1379
+ *
1380
+ * @returns {number} the severity of the best practice that corresponds to this accessibility concern
1381
+ */
1382
+ get severity() {
1383
+ return this._severity;
1384
+ }
1385
+
1386
+ set severity(severity) {
1387
+ this._severity = severity;
1388
+ }
1389
+
1390
+ /**
1391
+ * The noticeability of this accessibility concern on a scale of 1 to 10, where 1 is the least noticeable and 10 is the most noticeable.
1392
+ * Visit the URL returned by {@link AccessibilityConcern#bestPracticeDetailsUrl} for more information.
1393
+ *
1394
+ * @returns {number} the noticeability of the best practice that corresponds to this accessibility concern
1395
+ */
1396
+ get noticeability() {
1397
+ return this._noticeability;
1398
+ }
1399
+
1400
+ set noticeability(noticeability) {
1401
+ this._noticeability = noticeability;
1402
+ }
1403
+
1404
+ /**
1405
+ * The tractability of this accessibility concern on a scale of 1 to 10, where 1 is the least tractable and 10 is the most tractable.
1406
+ * Visit the URL returned by {@link AccessibilityConcern#bestPracticeDetailsUrl} for more information.
1407
+ *
1408
+ * @returns {number} the tractability of the best practice that corresponds to this accessibility concern
1409
+ */
1410
+ get tractability() {
1411
+ return this._tractability;
1412
+ }
1413
+
1414
+ set tractability(tractability) {
1415
+ this._tractability = tractability;
1416
+ }
1417
+
1418
+ /**
1419
+ * The URL of the best practice page in AMP that corresponds to this accessibility concern.
1420
+ * An AMP license is not required to visit this URL, but if you're logged into AMP, you'll be presented with additional information beyond what's publicly available.
1421
+ *
1422
+ * @returns {string} the URL of the best practice page in AMP that corresponds to this accessibility concern
1423
+ */
1424
+ get bestPracticeDetailsUrl() {
1425
+ return this._bestPracticeDetailsUrl;
1426
+ }
1427
+
1428
+ set bestPracticeDetailsUrl(bestPracticeDetailsUrl) {
1429
+ this._bestPracticeDetailsUrl = bestPracticeDetailsUrl;
1430
+ }
1431
+
1432
+ /**
1433
+ * An array of accessibility standards associated with the best practice that corresponds to this accessibility concern, ordered alphabetically by name.
1434
+ * Visit the URL returned by {@link AccessibilityConcern#bestPracticeDetailsUrl} for more information.
1435
+ *
1436
+ * @returns {Standard[]} an array of accessibility standards associated with the best practice that corresponds to this accessibility concern
1437
+ */
1438
+ get bestPracticeStandards() {
1439
+ return this._bestPracticeStandards;
1440
+ }
1441
+
1442
+ set bestPracticeStandards(bestPracticeStandards) {
1443
+ this._bestPracticeStandards = bestPracticeStandards;
1444
+ }
1445
+ }
1446
+
1447
+ /**
1448
+ * A class that encapsulates remediation steps suggested by Access Engine for resolving an accessibility concern.
1449
+ */
1450
+ class FixType {
1451
+
1452
+ /**
1453
+ * @constructor
1454
+ * @param {boolean} domSpec - defines whether this fix is specific to the particular page under test
1455
+ * @param {string} helperText - a brief human-readable description of how to resolve the accessibility concern corresponding to this fix
1456
+ * @returns {FixType}
1457
+ */
1458
+ constructor(domSpec, helperText) {
1459
+ this._domSpec = domSpec;
1460
+ this._helperText = helperText;
1461
+ }
1462
+
1463
+ set domSpec(domSpec) {
1464
+ this._domSpec = domSpec;
1465
+ }
1466
+
1467
+ /**
1468
+ * Defines whether this fix is specific to the particular page under test.
1469
+ *
1470
+ * @returns {boolean} whether this fix is specific to the particular page under test, or more general
1471
+ */
1472
+ get domSpec() {
1473
+ return this._domSpec;
1474
+ }
1475
+
1476
+ set helperText(helperText) {
1477
+ this._helperText = helperText;
1478
+ }
1479
+
1480
+ /**
1481
+ * A brief human-readable description of how to resolve the accessibility concern corresponding to this fix. Consult AMP for additional information.
1482
+ *
1483
+ * @returns {string} a brief human-readable description of how to resolve the accessibility concern corresponding to this fix
1484
+ */
1485
+ get helperText() {
1486
+ return this._helperText;
1487
+ }
1488
+ }
1489
+
1490
+ /**
1491
+ * A class that encapsulates accessibility standards associated with best practices returned by AMP.
1492
+ */
1493
+ class Standard {
1494
+
1495
+ /**
1496
+ * @constructor
1497
+ * @param {number} id - the ID of the accessibility standard
1498
+ * @param {string} name - the name of the accessibility standard
1499
+ * @returns {Standard}
1500
+ */
1501
+ constructor(id, name) {
1502
+ this._id = id;
1503
+ this._name = name;
1504
+ }
1505
+
1506
+ set id(id) {
1507
+ this._id = id;
1508
+ }
1509
+
1510
+ /**
1511
+ * Gets the ID of the accessibility standard as defined in AMP.
1512
+ *
1513
+ * @returns {number} the ID of the accessibility standard
1514
+ */
1515
+ get id() {
1516
+ return this._id;
1517
+ }
1518
+
1519
+ set name(name) {
1520
+ this._name = name;
1521
+ }
1522
+
1523
+ /**
1524
+ * Gets the name of the accessibility standard as defined in AMP.
1525
+ *
1526
+ * @returns {string} the name of the accessibility standard
1527
+ */
1528
+ get name() {
1529
+ return this._name;
1530
+ }
1531
+ }
1532
+
1533
+ /**
1534
+ * This class encapsulates all network requests Continuum itself makes to the Internet.
1535
+ *
1536
+ * @private
1537
+ */
1538
+ class NetworkUtil {
1539
+
1540
+ static getFromAMP(urlEndpointPath, queryParams, includeToken, driver, windowUnderTest) {
1541
+ return NetworkUtil._getFromAMP('GET', urlEndpointPath, queryParams, null, includeToken, driver, windowUnderTest);
1542
+ }
1543
+
1544
+ static postToAMP(urlEndpointPath, bodyParams, includeToken, driver, windowUnderTest) {
1545
+ return NetworkUtil._getFromAMP('POST', urlEndpointPath, null, bodyParams, includeToken, driver, windowUnderTest);
1546
+ }
1547
+
1548
+ static _getFromAMP(method, urlEndpointPath, queryParams, bodyParams, includeToken, driver, windowUnderTest) {
1549
+ if (includeToken) {
1550
+ if (!queryParams) {
1551
+ queryParams = {}
1552
+ }
1553
+ queryParams.apiToken = Configuration.getAmpApiToken();
1554
+ }
1555
+
1556
+ if (!urlEndpointPath) {
1557
+ urlEndpointPath = "";
1558
+ }
1559
+ const url = Configuration.getAmpInstanceUrl() + urlEndpointPath + NetworkUtil._formatQueryParams(queryParams);
1560
+
1561
+ if (driver) {
1562
+ return new Promise((resolve, reject) => {
1563
+ const ampInstanceUrlWithoutProtocol = Configuration.getAmpInstanceUrl().replace(/https?:\/\//, "");
1564
+
1565
+ const sendAMPRequest = (socket) => {
1566
+ const options = {
1567
+ host: ampInstanceUrlWithoutProtocol,
1568
+ port: 443,
1569
+ method: method,
1570
+ path: urlEndpointPath + NetworkUtil._formatQueryParams(queryParams),
1571
+ headers: {
1572
+ 'Content-Type': 'application/json;charset=UTF-8'
1573
+ },
1574
+ };
1575
+ if (socket) {
1576
+ options.socket = socket;
1577
+ options.agent = false;
1578
+ }
1579
+
1580
+ const req = require('https').request(options, (res) => {
1581
+ if (res.statusCode !== 200) {
1582
+ throw new HttpErrorException(`Unexpectedly encountered a non-200 status code (${res.statusCode} ${res.statusMessage}) while attempting to GET data from ${url}`);
1583
+ }
1584
+
1585
+ res.setEncoding('utf8');
1586
+
1587
+ let output = '';
1588
+ res.on('data', (chunk) => {
1589
+ output += chunk;
1590
+ });
1591
+ res.on('end', () => {
1592
+ resolve(output ? JSON.parse(output) : null);
1593
+ });
1594
+ });
1595
+ req.on('socket', (socket) => {
1596
+ socket.setTimeout(NetworkUtil._getTimeout());
1597
+ socket.on('timeout', () => {
1598
+ req.abort();
1599
+ });
1600
+ });
1601
+ req.on('error', (err) => {
1602
+ reject(err);
1603
+ });
1604
+ if (method === 'POST') {
1605
+ req.write(NetworkUtil._formatBodyParams(bodyParams));
1606
+ }
1607
+ req.end();
1608
+ };
1609
+
1610
+ if (Configuration.getProxyConfiguration() && Configuration.getProxyConfiguration().getHost()) {
1611
+ let credentials = "";
1612
+ if (Configuration.getProxyConfiguration().getUsername()) {
1613
+ credentials = `${Configuration.getProxyConfiguration().getUsername()}:${Configuration.getProxyConfiguration().getPassword()}@`;
1614
+ }
1615
+
1616
+ // enable the proxy, perform our network request, then disable the proxy
1617
+ require('global-agent/bootstrap');
1618
+ global.GLOBAL_AGENT.HTTP_PROXY = `http://${credentials}${Configuration.getProxyConfiguration().getHost()}:${Configuration.getProxyConfiguration().getPort()}`;
1619
+ try {
1620
+ sendAMPRequest();
1621
+ } finally {
1622
+ global.GLOBAL_AGENT.HTTP_PROXY = '';
1623
+ }
1624
+ } else {
1625
+ sendAMPRequest();
1626
+ }
1627
+ });
1628
+ } else if (windowUnderTest) {
1629
+ return new Promise((resolve, reject) => {
1630
+ const request = new XMLHttpRequest();
1631
+ request.open(method, url, true);
1632
+ request.setRequestHeader('Content-Type', "application/json;charset=UTF-8");
1633
+ request.timeout = NetworkUtil._getTimeout();
1634
+ request.onload = () => {
1635
+ if (request.readyState === 4) {
1636
+ if (request.status === 200) {
1637
+ resolve(request.responseText ? JSON.parse(request.responseText) : null);
1638
+ } else {
1639
+ throw new HttpErrorException(`Unexpectedly encountered a non-200 status code (${request.status} ${request.statusText}) while attempting to GET data from ${url}`);
1640
+ }
1641
+ }
1642
+ };
1643
+ request.onerror = (err) => {
1644
+ reject(err);
1645
+ };
1646
+ request.send(bodyParams ? NetworkUtil._formatBodyParams(bodyParams) : null);
1647
+ });
1648
+ }
1649
+ }
1650
+
1651
+ static _getTimeout() {
1652
+ return 60000; // in milliseconds
1653
+ }
1654
+
1655
+ static _formatQueryParams(queryParams) {
1656
+ if (!queryParams || Object.keys(queryParams).length <= 0) {
1657
+ return "";
1658
+ }
1659
+
1660
+ return "?" + Object.keys(queryParams).map((key) => {
1661
+ return [key, queryParams[key]].map(encodeURIComponent).join("=");
1662
+ }).join("&");
1663
+ }
1664
+
1665
+ static _formatBodyParams(bodyParams) {
1666
+ if (!bodyParams || Object.keys(bodyParams).length <= 0) {
1667
+ return null;
1668
+ }
1669
+
1670
+ return JSON.stringify(bodyParams);
1671
+ }
1672
+ }
1673
+
1674
+ /**
1675
+ * This class encapsulates all of functionality for submitting accessibility concerns identified using Continuum to AMP.
1676
+ *
1677
+ * Reporting test results from Continuum to AMP is accomplished through a kind of state machine, where you set the active AMP instance, organization, asset, report, and module to use; once these are set, they remain set for as long as they're not set again and for as long as Continuum is initialized.
1678
+ * Depending on the report and module management strategies you decide to use—see {@link ReportManagementStrategy} and {@link ModuleManagementStrategy}, respectively—invoking {@link AMPReportingService#submitAccessibilityConcernsToAMP} will first create, overwrite, and/or delete reports and modules from AMP, then publish your test results to the active AMP module.
1679
+ * You can set the active report and module management strategies using {@link AMPReportingService#setActiveReportManagementStrategy} and {@link AMPReportingService#setActiveModuleManagementStrategy}, respectively.
1680
+ * Only once all of these active items are set should you invoke {@link AMPReportingService#submitAccessibilityConcernsToAMP} using the list of accessibility concerns you'd like to report.
1681
+ *
1682
+ * More on report and module management strategies: they are designed with two primary use cases in mind: continuous integration (CI) workflows (where you usually want to retain the results of previously published reports), and more manual workflows (e.g. when Continuum is run from a developer's local workstation, where you usually don't want to retain the results of previously published reports).
1683
+ * Choosing the correct report and module management strategies to meet your business objectives is critical to using Continuum's AMP reporting functionality correctly, so please consult our support documentation for more information.
1684
+ *
1685
+ * @hideconstructor
1686
+ */
1687
+ class AMPReportingService {
1688
+
1689
+ /**
1690
+ * @constructor
1691
+ * @returns {AMPReportingService}
1692
+ */
1693
+ constructor(driver, windowUnderTest) {
1694
+ this._activeInstance = Configuration.getAmpInstanceUrl();
1695
+ this._activeOrganizationId = null;
1696
+ this._activeAssetId = null;
1697
+ this._activeReport = null;
1698
+ this._activeModule = null;
1699
+ this._activeReportManagementStrategy = null;
1700
+ this._activeModuleManagementStrategy = null;
1701
+
1702
+ this._driver = driver;
1703
+ this._windowUnderTest = windowUnderTest;
1704
+ }
1705
+
1706
+ /**
1707
+ * @private
1708
+ * @returns {string}
1709
+ */
1710
+ get activeInstance() {
1711
+ return this._activeInstance;
1712
+ }
1713
+
1714
+ set activeInstance(activeInstance) {
1715
+ this._activeInstance = activeInstance;
1716
+ }
1717
+
1718
+ /**
1719
+ * @private
1720
+ * @returns {number}
1721
+ */
1722
+ get activeOrganizationId() {
1723
+ return this._activeOrganizationId;
1724
+ }
1725
+
1726
+ set activeOrganizationId(activeOrganizationId) {
1727
+ this._activeOrganizationId = activeOrganizationId;
1728
+ }
1729
+
1730
+ /**
1731
+ * @private
1732
+ * @returns {number}
1733
+ */
1734
+ get activeAssetId() {
1735
+ return this._activeAssetId;
1736
+ }
1737
+
1738
+ set activeAssetId(activeAssetId) {
1739
+ this._activeAssetId = activeAssetId;
1740
+ }
1741
+
1742
+ /**
1743
+ * @private
1744
+ * @returns {boolean}
1745
+ */
1746
+ get suppressSensitiveData() {
1747
+ return this._suppressSensitiveData;
1748
+ }
1749
+
1750
+ set suppressSensitiveData(suppressSensitiveData) {
1751
+ this._suppressSensitiveData = suppressSensitiveData;
1752
+ }
1753
+
1754
+ /**
1755
+ * Gets the active report.
1756
+ * This is null if {@link setActiveReportById} or {@link setActiveReportByName} hasn't been invoked to set an active report yet.
1757
+ * Use this to access the active report's metadata, e.g. its ID in AMP, its name, etc.
1758
+ *
1759
+ * @see Report
1760
+ * @returns {?Report}
1761
+ */
1762
+ get activeReport() {
1763
+ return this._activeReport;
1764
+ }
1765
+
1766
+ /**
1767
+ * @private
1768
+ * @param activeReport
1769
+ */
1770
+ set activeReport(activeReport) {
1771
+ this._activeReport = activeReport;
1772
+ }
1773
+
1774
+ /**
1775
+ * Gets the active module.
1776
+ * This is null if {@link setActiveModuleById} or {@link setActiveModuleByName} hasn't been invoked to set an active module yet.
1777
+ * Use this to access the active module's metadata, e.g. its ID in AMP, its name, etc.
1778
+ *
1779
+ * @see Module
1780
+ * @returns {?Module}
1781
+ */
1782
+ get activeModule() {
1783
+ return this._activeModule;
1784
+ }
1785
+
1786
+ /**
1787
+ * @private
1788
+ * @param activeModule
1789
+ */
1790
+ set activeModule(activeModule) {
1791
+ this._activeModule = activeModule;
1792
+ }
1793
+
1794
+ /**
1795
+ * @private
1796
+ * @returns {ReportManagementStrategy}
1797
+ */
1798
+ get activeReportManagementStrategy() {
1799
+ return this._activeReportManagementStrategy;
1800
+ }
1801
+
1802
+ set activeReportManagementStrategy(activeReportManagementStrategy) {
1803
+ this._activeReportManagementStrategy = activeReportManagementStrategy;
1804
+ }
1805
+
1806
+ /**
1807
+ * @private
1808
+ * @returns {ModuleManagementStrategy}
1809
+ */
1810
+ get activeModuleManagementStrategy() {
1811
+ return this._activeModuleManagementStrategy;
1812
+ }
1813
+
1814
+ set activeModuleManagementStrategy(activeModuleManagementStrategy) {
1815
+ this._activeModuleManagementStrategy = activeModuleManagementStrategy;
1816
+ }
1817
+
1818
+ /**
1819
+ * @private
1820
+ * @returns {*}
1821
+ */
1822
+ get driver() {
1823
+ return this._driver;
1824
+ }
1825
+
1826
+ set driver(driver) {
1827
+ this._driver = driver;
1828
+ }
1829
+
1830
+ /**
1831
+ * @private
1832
+ * @returns {Window}
1833
+ */
1834
+ get windowUnderTest() {
1835
+ return this._windowUnderTest;
1836
+ }
1837
+
1838
+ set windowUnderTest(window) {
1839
+ this._windowUnderTest = window;
1840
+ }
1841
+
1842
+ /**
1843
+ * Validates the specified ID of an existing organization in AMP, then sets it as the active organization in Continuum such that next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked, test results will be submitted to this active organization.
1844
+ *
1845
+ * @param {number} organizationId - the ID of the AMP organization to make active
1846
+ * @throws {IllegalArgumentException} if the specified organization ID is null
1847
+ * @throws {HttpErrorException} if an error is encountered while attempting to connect to AMP to validate the specified organization ID
1848
+ * @throws {NotFoundException} if the specified organization may not exist in the active AMP instance or is otherwise not accessible
1849
+ */
1850
+ async setActiveOrganization(organizationId) {
1851
+ if (!organizationId) {
1852
+ throw new IllegalArgumentException("Active organization cannot be null");
1853
+ }
1854
+
1855
+ const responseJson = await NetworkUtil.getFromAMP("/api/cont/organization/validate", {
1856
+ organizationId: organizationId.toString()
1857
+ }, true, this.driver, this.windowUnderTest);
1858
+
1859
+ if (!responseJson.valid) {
1860
+ const message = responseJson.message ? ("; " + responseJson.message) : "";
1861
+ throw new NotFoundException(`Organization with ID '${organizationId}' not found in active AMP instance '${this.activeInstance}'${message}`);
1862
+ }
1863
+
1864
+ this.activeOrganizationId = organizationId;
1865
+ }
1866
+
1867
+ /**
1868
+ * Validates the specified ID of an existing asset in AMP, then sets it as the active asset in Continuum such that next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked, test results will be submitted to this active asset.
1869
+ * Make sure you first set the active organization for this asset prior to invoking this function using {@link AMPReportingService#setActiveOrganization}.
1870
+ *
1871
+ * @param {number} assetId - the ID of the AMP asset to make active
1872
+ * @throws {IllegalStateException} if the active organization is not set
1873
+ * @throws {IllegalArgumentException} if the specified asset ID is null
1874
+ * @throws {HttpErrorException} if an error is encountered while attempting to connect to AMP to validate the specified asset ID
1875
+ * @throws {NotFoundException} if the specified asset may not exist in the active AMP instance or is otherwise not accessible
1876
+ */
1877
+ async setActiveAsset(assetId) {
1878
+ if (!this.activeOrganizationId) {
1879
+ throw new IllegalStateException("Active organization has not been set");
1880
+ }
1881
+
1882
+ if (!assetId) {
1883
+ throw new IllegalArgumentException("Active asset cannot be null");
1884
+ }
1885
+
1886
+ const responseJson = await NetworkUtil.getFromAMP("/api/cont/asset/validate", {
1887
+ assetId: assetId.toString()
1888
+ }, true, this.driver, this.windowUnderTest);
1889
+
1890
+ if (!responseJson.valid) {
1891
+ const message = responseJson.message ? ("; " + responseJson.message) : "";
1892
+ throw new NotFoundException(`Asset with ID '${assetId}' not found in active AMP instance '${this.activeInstance}'${message}`);
1893
+ }
1894
+
1895
+ this.activeAssetId = assetId;
1896
+ this.suppressSensitiveData = (responseJson.suppressSensitiveData != null) ? responseJson.suppressSensitiveData : false;
1897
+ }
1898
+
1899
+ /**
1900
+ * Validates the specified ID of an existing report in AMP, then sets it as the active report in Continuum such that next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked, test results will be submitted to this active report.
1901
+ * Make sure you first set the active organization and asset for this report prior to invoking this function using {@link AMPReportingService#setActiveOrganization} and {@link AMPReportingService#setActiveAsset}, respectively.
1902
+ *
1903
+ * @param {number} reportId - the ID of the AMP report to make active
1904
+ * @throws {IllegalStateException} if the active organization or asset is not set
1905
+ * @throws {IllegalArgumentException} if the specified report ID is null
1906
+ * @throws {HttpErrorException} if an error is encountered while attempting to connect to AMP to validate the specified report ID
1907
+ * @throws {NotFoundException} if the specified report may not exist in the active AMP instance or is otherwise not accessible
1908
+ */
1909
+ async setActiveReportById(reportId) {
1910
+ if (!this.activeOrganizationId) {
1911
+ throw new IllegalStateException("Active organization has not been set");
1912
+ }
1913
+
1914
+ if (!this.activeAssetId) {
1915
+ throw new IllegalStateException("Active asset has not been set");
1916
+ }
1917
+
1918
+ if (!reportId) {
1919
+ throw new IllegalArgumentException("Active report cannot be null");
1920
+ }
1921
+
1922
+ let reportName = null;
1923
+
1924
+ const responseJson = await NetworkUtil.getFromAMP("/api/cont/report/validate", {
1925
+ assetId: this.activeAssetId.toString(),
1926
+ reportId: reportId.toString()
1927
+ }, true, this.driver, this.windowUnderTest);
1928
+
1929
+ if (!responseJson.valid) {
1930
+ const message = responseJson.message ? ("; " + responseJson.message) : "";
1931
+ throw new NotFoundException(`Report with ID '${reportId}' not found in active AMP instance '${this.activeInstance}'${message}`);
1932
+ }
1933
+
1934
+ reportName = responseJson.reportName;
1935
+
1936
+ this.activeReport = new Report(reportId, reportName);
1937
+ }
1938
+
1939
+ /**
1940
+ * Sets the active report in AMP to submit test results to next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked.
1941
+ * The report name specified is validated, but unlike {@link AMPReportingService#setActiveReportById}, this method will not throw an exception if the specified report does not yet exist in AMP; it will be created next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked.
1942
+ * Make sure you first set the active organization and asset for this report prior to invoking this function using {@link AMPReportingService#setActiveOrganization} and {@link AMPReportingService#setActiveAsset}, respectively.
1943
+ *
1944
+ * @param {string} reportName - the name of the AMP report to make active
1945
+ * @throws {IllegalStateException} if the active organization or asset is not set
1946
+ * @throws {IllegalArgumentException} if the specified report name is null
1947
+ * @throws {HttpErrorException} if an error is encountered while attempting to connect to AMP to validate the specified report name
1948
+ * @returns {number} the ID of the AMP report, if it already exists; null if the report does not yet exist in AMP
1949
+ */
1950
+ async setActiveReportByName(reportName) {
1951
+ if (!this.activeOrganizationId) {
1952
+ throw new IllegalStateException("Active organization has not been set");
1953
+ }
1954
+
1955
+ if (!this.activeAssetId) {
1956
+ throw new IllegalStateException("Active asset has not been set");
1957
+ }
1958
+
1959
+ if (!reportName) {
1960
+ throw new IllegalArgumentException("Active report cannot be null");
1961
+ }
1962
+
1963
+ let reportId = null;
1964
+
1965
+ const responseJson = await NetworkUtil.getFromAMP("/api/cont/report/validate", {
1966
+ assetId: this.activeAssetId.toString(),
1967
+ reportName: reportName
1968
+ }, true, this.driver, this.windowUnderTest);
1969
+
1970
+ if (responseJson.valid && responseJson.reportId) {
1971
+ reportId = responseJson.reportId;
1972
+ }
1973
+
1974
+ this.activeReport = new Report(reportId, reportName);
1975
+ return reportId;
1976
+ }
1977
+
1978
+ /**
1979
+ * Validates the specified ID of an existing module in AMP, then sets it as the active module in Continuum such that next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked, test results will be submitted to this active module.
1980
+ * Make sure you first set the active organization, asset, and report for this module prior to invoking this function using {@link AMPReportingService#setActiveOrganization}, {@link AMPReportingService#setActiveAsset}, and {@link AMPReportingService#setActiveReportById} or {@link AMPReportingService#setActiveReportByName}, respectively.
1981
+ * While using {@link ReportManagementStrategy#OVERWRITE} as your report management strategy, use {@link AMPReportingService#setActiveModuleByName} instead of this method; see the documentation for {@link ReportManagementStrategy#OVERWRITE} for details as to why.
1982
+ *
1983
+ * @param {number} moduleId - the ID of the AMP module to make active
1984
+ * @throws {IllegalStateException} if the active organization, asset, or report is not set
1985
+ * @throws {IllegalArgumentException} if the specified module ID is null
1986
+ * @throws {HttpErrorException} if an error is encountered while attempting to connect to AMP to validate the specified module ID
1987
+ * @throws {NotFoundException} if the specified module may not exist in the active AMP report
1988
+ */
1989
+ async setActiveModuleById(moduleId) {
1990
+ if (!this.activeOrganizationId) {
1991
+ throw new IllegalStateException("Active organization has not been set");
1992
+ }
1993
+
1994
+ if (!this.activeAssetId) {
1995
+ throw new IllegalStateException("Active asset has not been set");
1996
+ }
1997
+
1998
+ if (!this.activeReport || !this.activeReport.id) {
1999
+ throw new IllegalStateException("Active report has not been set");
2000
+ }
2001
+
2002
+ if (!moduleId) {
2003
+ throw new IllegalArgumentException("Active module cannot be null");
2004
+ }
2005
+
2006
+ let moduleName = null;
2007
+ let moduleLocation = null;
2008
+
2009
+ const responseJson = await NetworkUtil.getFromAMP("/api/cont/module/validate", {
2010
+ assetId: this.activeAssetId.toString(),
2011
+ reportId: this.activeReport.id.toString(),
2012
+ moduleId: moduleId.toString()
2013
+ }, true, this.driver, this.windowUnderTest);
2014
+
2015
+ if (!responseJson.valid) {
2016
+ const message = responseJson.message ? ("; " + responseJson.message) : "";
2017
+ throw new NotFoundException(`Module with ID '${moduleId}' not found in active AMP instance '${this.activeInstance}'${message}`);
2018
+ }
2019
+
2020
+ moduleName = responseJson.moduleName;
2021
+ moduleLocation = responseJson.moduleLocation;
2022
+
2023
+ this.activeModule = new Module(moduleId, moduleName, moduleLocation);
2024
+ }
2025
+
2026
+ /**
2027
+ * Sets the active module in AMP to submit test results to next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked.
2028
+ * The module name specified is validated if the active report already exists in AMP, but unlike {@link AMPReportingService#setActiveModuleById}, this method will not throw an exception if the specified module does not yet exist in AMP; it will be created next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked.
2029
+ * Make sure you first set the active organization, asset, and report for this module prior to invoking this function using {@link AMPReportingService#setActiveOrganization}, {@link AMPReportingService#setActiveAsset}, and {@link AMPReportingService#setActiveReportById} or {@link AMPReportingService#setActiveReportByName}, respectively.
2030
+ *
2031
+ * @param {string} moduleName - the name of the AMP module to make active
2032
+ * @param {string} moduleLocation - the name of the location in the website or app being tested; this can be a fully qualified URL, or simply a page title like "Login Page"
2033
+ * @throws {IllegalStateException} if the active organization, asset, or report is not set
2034
+ * @throws {IllegalArgumentException} if the specified module name or location is null
2035
+ * @throws {HttpErrorException} if an error is encountered while attempting to connect to AMP to validate the specified module name
2036
+ * @returns {number} the ID of the AMP module, if it already exists; null if the module does not yet exist in AMP
2037
+ */
2038
+ async setActiveModuleByName(moduleName, moduleLocation) {
2039
+ if (!this.activeOrganizationId) {
2040
+ throw new IllegalStateException("Active organization has not been set");
2041
+ }
2042
+
2043
+ if (!this.activeAssetId) {
2044
+ throw new IllegalStateException("Active asset has not been set");
2045
+ }
2046
+
2047
+ if (!this.activeReport) {
2048
+ throw new IllegalStateException("Active report has not been set");
2049
+ }
2050
+
2051
+ if (!moduleName) {
2052
+ throw new IllegalArgumentException("Active module cannot be null");
2053
+ }
2054
+
2055
+ if (!moduleLocation) {
2056
+ throw new IllegalArgumentException("Active module location cannot be null");
2057
+ }
2058
+
2059
+ let moduleId = null;
2060
+
2061
+ // only attempt to validate this module if we have the necessary report ID to do so
2062
+ if (this.activeReport.id) {
2063
+ const responseJson = await NetworkUtil.getFromAMP("/api/cont/module/validate", {
2064
+ assetId: this.activeAssetId.toString(),
2065
+ reportId: this.activeReport.id.toString(),
2066
+ moduleName: moduleName
2067
+ }, true, this.driver, this.windowUnderTest);
2068
+
2069
+ if (responseJson.valid && responseJson.moduleId) {
2070
+ moduleId = responseJson.moduleId;
2071
+ }
2072
+ }
2073
+
2074
+ this.activeModule = new Module(moduleId, moduleName, moduleLocation);
2075
+ return moduleId;
2076
+ }
2077
+
2078
+ /**
2079
+ * Sets the active report management strategy to use next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked.
2080
+ * Choosing the correct report and module management strategies to meet your business objectives is critical to using Continuum's AMP reporting functionality correctly, so please consult our support documentation for more information.
2081
+ *
2082
+ * @param {ReportManagementStrategy} reportManagementStrategy - the preferred management strategy to use when creating and editing AMP reports
2083
+ */
2084
+ setActiveReportManagementStrategy(reportManagementStrategy) {
2085
+ this.activeReportManagementStrategy = reportManagementStrategy;
2086
+ }
2087
+
2088
+ /**
2089
+ * Sets the active module management strategy to use next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked.
2090
+ * Choosing the correct report and module management strategies to meet your business objectives is critical to using Continuum's AMP reporting functionality correctly, so please consult our support documentation for more information.
2091
+ *
2092
+ * @param {ModuleManagementStrategy} moduleManagementStrategy - the preferred management strategy to use when creating and editing AMP modules
2093
+ */
2094
+ setActiveModuleManagementStrategy(moduleManagementStrategy) {
2095
+ this.activeModuleManagementStrategy = moduleManagementStrategy;
2096
+ }
2097
+
2098
+ /**
2099
+ * Submits accessibility concerns to the active AMP instance, organization, asset, report, and module.
2100
+ * Make sure to set the active AMP organization (via {@link AMPReportingService#setActiveOrganization}), asset (via {@link AMPReportingService#setActiveAsset}), report (via {@link AMPReportingService#setActiveReportById} or {@link AMPReportingService#setActiveReportByName}), and module (via {@link AMPReportingService#setActiveModuleById} or {@link AMPReportingService#setActiveModuleByName}) prior to invoking this function.
2101
+ * The active instance, organization, and asset must all already exist in AMP prior to invoking this function, otherwise an exception will be thrown; reports and modules don't need to exist in AMP yet, as they will be created if necessary.
2102
+ * Also, make sure to set your desired report and module management strategies prior to invoking this function using {@link AMPReportingService#setActiveReportManagementStrategy} and {@link AMPReportingService#setActiveModuleManagementStrategy}, respectively, according to your use case.
2103
+ * Choosing the correct report and module management strategies to meet your business objectives is critical to using Continuum's AMP reporting functionality correctly, so please consult our support documentation for more information.
2104
+ *
2105
+ * @param {AccessibilityConcern[]} accessibilityConcerns - the list of accessibility concerns to submit to AMP
2106
+ * @throws {IllegalStateException} if the active instance, organization, asset, report, or module is not set
2107
+ * @throws {HttpErrorException} if an error is encountered while attempting to connect to AMP to validate the active organization, asset, report, or module
2108
+ * @throws {NotFoundException} if the active instance, organization, or asset may not exist in AMP as specified, or if an error occurs while attempting to create the necessary report or module in AMP (if applicable)
2109
+ * @returns {boolean} true if uploading of the specified accessibility concerns to AMP succeeded; false if it did not
2110
+ */
2111
+ async submitAccessibilityConcernsToAMP(accessibilityConcerns) {
2112
+ // validate active organization
2113
+ await this.setActiveOrganization(this.activeOrganizationId);
2114
+
2115
+ // validate active asset
2116
+ await this.setActiveAsset(this.activeAssetId);
2117
+
2118
+ // validate or create active report
2119
+ let reportExistsInAMP = false;
2120
+ if (this.activeReport && this.activeReport.id && this.activeReportManagementStrategy !== ReportManagementStrategy.UNIQUE) {
2121
+ // the active report already has an ID, so verify it still exists in AMP; if it doesn't, abort
2122
+ await this.setActiveReportById(this.activeReport.id);
2123
+ reportExistsInAMP = true;
2124
+ } else {
2125
+ // user has requested we guarantee the creation of a new report by modifying the report name they've specified
2126
+ if (this.activeReport && this.activeReport.name && this.activeReportManagementStrategy === ReportManagementStrategy.UNIQUE) {
2127
+ this.activeReport.name += ` (${new Date().toISOString()})`;
2128
+ }
2129
+
2130
+ // the active report may or may not exist in AMP as no ID was specified, so check and create it if necessary
2131
+ const reportId = await this.setActiveReportByName(this.activeReport ? this.activeReport.name : null);
2132
+ if (!reportId) {
2133
+ this.activeReport.id = await this._createReport(this.activeReport.name);
2134
+ if (!this.activeReport.id) {
2135
+ throw new NotFoundException(`Could not create new report '${this.activeReport.name}' in AMP`);
2136
+ }
2137
+
2138
+ if (this.activeModule) {
2139
+ // when the time comes, ensure a new module is created for this new report
2140
+ this.activeModule.id = null;
2141
+ }
2142
+ } else {
2143
+ this.activeReport.id = reportId;
2144
+ reportExistsInAMP = true;
2145
+ }
2146
+ }
2147
+ if (reportExistsInAMP) {
2148
+ if (this.activeReportManagementStrategy === ReportManagementStrategy.OVERWRITE) {
2149
+ const success = await this._deleteAllModulesInActiveReport();
2150
+ if (success) {
2151
+ // assuming the active module already existed in the active report when they were specified by the user, the active module's ID is no longer valid as this module was just deleted from AMP
2152
+ // clearing out the active module ID here means the active module will get recreated in AMP later on in the same active report as though the user had not specified an ID, which is what we want
2153
+ this.activeModule.id = null;
2154
+ } else {
2155
+ const reportIdentifierText = this.activeReport.name ? `'${this.activeReport.name}'` : `ID '${this.activeReport.id}'`;
2156
+ throw new NotFoundException(`Could not delete existing modules from report ${reportIdentifierText} in AMP`);
2157
+ }
2158
+ }
2159
+ }
2160
+
2161
+ // validate or create active module
2162
+ let moduleExistsInAMP = false;
2163
+ let overwriteExistingAccessibilityConcerns = false;
2164
+ if (this.activeModule && this.activeModule.id) {
2165
+ // the active module already has an ID, so verify it still exists in AMP
2166
+ await this.setActiveModuleById(this.activeModule.id);
2167
+ moduleExistsInAMP = true;
2168
+ } else {
2169
+ // the active module may or may not exist in AMP as no ID was specified, so check and create it if necessary
2170
+ const moduleId = await this.setActiveModuleByName(this.activeModule ? this.activeModule.name : null, this.activeModule ? this.activeModule.location : null);
2171
+ if (!moduleId) {
2172
+ // module does not yet exist in AMP, so create it
2173
+ this.activeModule.id = await this._createModule(this.activeModule.name);
2174
+ if (!this.activeModule.id) {
2175
+ throw new NotFoundException(`Could not create new module '${this.activeModule.name}' in AMP`);
2176
+ }
2177
+ } else {
2178
+ this.activeModule.id = moduleId;
2179
+ moduleExistsInAMP = true;
2180
+ }
2181
+ }
2182
+ if (moduleExistsInAMP) {
2183
+ if (this.activeModuleManagementStrategy === ModuleManagementStrategy.ABORT) {
2184
+ console.log("The active module already exists in AMP! Aborting reporting to AMP per specified module management strategy of ABORT.");
2185
+ return false;
2186
+ } else if (this.activeModuleManagementStrategy === ModuleManagementStrategy.OVERWRITE) {
2187
+ overwriteExistingAccessibilityConcerns = true;
2188
+ }
2189
+ }
2190
+
2191
+ // at this point, the active organization, asset, report, and module should all exist in AMP (if they didn't already before)
2192
+
2193
+ if (!this.suppressSensitiveData) {
2194
+ // if possible, submit module screenshot to AMP
2195
+ if (this.driver && ('takeScreenshot' in this.driver) && (typeof this.driver.takeScreenshot === 'function')) {
2196
+ const screenshotDataURI = `data:image/png;base64,${await this.driver.takeScreenshot()}`;
2197
+ await NetworkUtil.postToAMP("/api/cont/module/screenshot", {
2198
+ moduleID: this.activeModule.id.toString(),
2199
+ data: screenshotDataURI
2200
+ }, true, this.driver, this.windowUnderTest);
2201
+ }
2202
+ }
2203
+
2204
+ // convert accessibility concerns into the format AMP expects for uploading
2205
+ const records = this._convertAccessibilityConcernsToAMPFormat(accessibilityConcerns);
2206
+
2207
+ // actually submit test results to AMP
2208
+
2209
+ const bodyParams = {
2210
+ reportID: this.activeReport.id.toString(),
2211
+ moduleID: this.activeModule.id.toString(),
2212
+ overwrite: overwriteExistingAccessibilityConcerns.toString(),
2213
+ records: records
2214
+ };
2215
+ if (this.activeModule.name) {
2216
+ bodyParams.moduleName = this.activeModule.name;
2217
+ }
2218
+ if (this.activeModule.location) {
2219
+ bodyParams.moduleLocation = this.activeModule.location;
2220
+ }
2221
+
2222
+ const responseJson = await NetworkUtil.postToAMP("/api/cont/module/upload", bodyParams, true, this.driver, this.windowUnderTest);
2223
+ return !!responseJson.moduleId;
2224
+ }
2225
+
2226
+ /**
2227
+ * Creates a new report in the active AMP asset.
2228
+ *
2229
+ * @private
2230
+ * @param {string} reportName - the name of the AMP report to create
2231
+ * @throws {IllegalStateException} if the active organization or asset is not set
2232
+ * @throws {IllegalArgumentException} if the specified report name is null
2233
+ * @throws {HttpErrorException} if an error is encountered while attempting to connect to AMP
2234
+ * @throws {NotFoundException} if the active report cannot be created in AMP
2235
+ * @returns {number} the ID of the AMP report created; null if the AMP report could not be created
2236
+ */
2237
+ async _createReport(reportName) {
2238
+ if (!this.activeOrganizationId) {
2239
+ throw new IllegalStateException("Active organization has not been set");
2240
+ }
2241
+
2242
+ if (!this.activeAssetId) {
2243
+ throw new IllegalStateException("Active asset has not been set");
2244
+ }
2245
+
2246
+ if (!reportName) {
2247
+ throw new IllegalArgumentException("Active report cannot be null");
2248
+ }
2249
+
2250
+ let reportId = null;
2251
+
2252
+ const responseJson = await NetworkUtil.postToAMP("/api/cont/report/create", {
2253
+ assetId: this.activeAssetId,
2254
+ reportName: reportName,
2255
+ mediaTypeId: WEB_MEDIA_TYPE_ID
2256
+ }, true, this.driver, this.windowUnderTest);
2257
+
2258
+ if (!responseJson.valid && responseJson.message) {
2259
+ throw new NotFoundException(responseJson.message);
2260
+ }
2261
+
2262
+ if (responseJson.valid && responseJson.reportId) {
2263
+ reportId = responseJson.reportId;
2264
+ }
2265
+
2266
+ return reportId;
2267
+ }
2268
+
2269
+ /**
2270
+ * Deletes any and all modules from the active AMP report.
2271
+ *
2272
+ * @private
2273
+ * @throws {IllegalStateException} if the active organization, asset, or report is not set
2274
+ * @throws {HttpErrorException} if an error is encountered while attempting to connect to AMP
2275
+ * @throws {NotFoundException} if the active report in AMP cannot be accessed or found
2276
+ * @returns {boolean} true if the deletion of all the active AMP report's modules succeeded; false if it did not
2277
+ */
2278
+ async _deleteAllModulesInActiveReport() {
2279
+ if (!this.activeOrganizationId) {
2280
+ throw new IllegalStateException("Active organization has not been set");
2281
+ }
2282
+
2283
+ if (!this.activeAssetId) {
2284
+ throw new IllegalStateException("Active asset has not been set");
2285
+ }
2286
+
2287
+ if (!this.activeReport || !this.activeReport.id) {
2288
+ throw new IllegalStateException("Active report has not been set");
2289
+ }
2290
+
2291
+ const responseJson = await NetworkUtil.postToAMP("/api/cont/report/overwrite", {
2292
+ assetId: this.activeAssetId,
2293
+ reportId: this.activeReport.id
2294
+ }, true, this.driver, this.windowUnderTest);
2295
+
2296
+ if (!responseJson) {
2297
+ // this endpoint doesn't send back a response if it succeeds
2298
+ return true;
2299
+ }
2300
+
2301
+ if (!responseJson.valid && responseJson.message) {
2302
+ throw new NotFoundException(responseJson.message);
2303
+ }
2304
+
2305
+ return false;
2306
+ }
2307
+
2308
+ /**
2309
+ * Creates a new module in the active AMP report.
2310
+ *
2311
+ * @private
2312
+ * @param {string} moduleName - the name of the AMP module to create
2313
+ * @throws {IllegalStateException} if the active organization, asset, or report is not set, or if the active report is set but doesn't yet exist in AMP
2314
+ * @throws {IllegalArgumentException} if the specified module name is null
2315
+ * @throws {HttpErrorException} if an error is encountered while attempting to connect to AMP
2316
+ * @throws {NotFoundException} if the active module cannot be created in AMP
2317
+ * @returns {number} the ID of the AMP module created; null if the AMP module could not be created
2318
+ */
2319
+ async _createModule(moduleName) {
2320
+ if (!this.activeOrganizationId) {
2321
+ throw new IllegalStateException("Active organization has not been set");
2322
+ }
2323
+
2324
+ if (!this.activeAssetId) {
2325
+ throw new IllegalStateException("Active asset has not been set");
2326
+ }
2327
+
2328
+ if (!this.activeReport || !this.activeReport.id) {
2329
+ throw new IllegalStateException("Active report has not been set");
2330
+ }
2331
+
2332
+ if (!moduleName) {
2333
+ throw new IllegalArgumentException("Active module cannot be null");
2334
+ }
2335
+
2336
+ let moduleId = null;
2337
+
2338
+ const responseJson = await NetworkUtil.postToAMP("/api/cont/module/create", {
2339
+ assetId: this.activeAssetId.toString(),
2340
+ reportId: this.activeReport.id.toString(),
2341
+ moduleName: moduleName
2342
+ }, true, this.driver, this.windowUnderTest);
2343
+
2344
+ if (!responseJson.valid && responseJson.message) {
2345
+ throw new NotFoundException(responseJson.message);
2346
+ }
2347
+
2348
+ if (responseJson.valid && responseJson.moduleId) {
2349
+ moduleId = responseJson.moduleId;
2350
+ }
2351
+
2352
+ return moduleId;
2353
+ }
2354
+
2355
+ /**
2356
+ * Converts accessibility concerns to the format AMP expects for reporting purposes.
2357
+ *
2358
+ * @private
2359
+ * @param {AccessibilityConcern[]} accessibilityConcerns - the accessibility concerns to convert
2360
+ * @returns {object} a JSON object that includes the specified accessibility concerns in a particular format
2361
+ */
2362
+ _convertAccessibilityConcernsToAMPFormat(accessibilityConcerns) {
2363
+ const records = {};
2364
+
2365
+ for (let i = 0; i < accessibilityConcerns.length; i++) {
2366
+ const accessibilityConcern = accessibilityConcerns[i];
2367
+ const result = accessibilityConcern.rawEngineJsonObject;
2368
+
2369
+ const instance = {};
2370
+
2371
+ let element = result.element;
2372
+ if (element) {
2373
+ if (this.suppressSensitiveData) {
2374
+ const match = element.match(/^<[^\s]+/i);
2375
+ if (match) {
2376
+ element = `<${match[0].substring(1).toLowerCase()}/>`;
2377
+ } else {
2378
+ element = '<unknown/>';
2379
+ }
2380
+ }
2381
+
2382
+ instance.element = element.substring(0, Math.min(element.length, 3000));
2383
+ }
2384
+
2385
+ const attributeDetail = this.suppressSensitiveData ? result.attribute : result.attributeDetail;
2386
+ instance.attribute = attributeDetail.substring(0, Math.min(attributeDetail.length, 3000));
2387
+
2388
+ instance.xpath = result.path;
2389
+ instance.testResult = result.testResult;
2390
+ instance.engineTestId = result.engineTestId;
2391
+
2392
+ // pass along stuff used by Alchemy
2393
+ const fixType = result.fixType;
2394
+ if (typeof fixType === 'object') {
2395
+ instance.fixType = fixType.fixType;
2396
+ instance.fix = fixType.fix;
2397
+ instance.fingerprint = result.fingerprint;
2398
+ }
2399
+
2400
+ const bestPracticeIdString = result.bestPracticeId;
2401
+ let record = records[bestPracticeIdString];
2402
+ if (!record) {
2403
+ record = {};
2404
+
2405
+ const violation = {};
2406
+ violation.violationID = result.bestPracticeId;
2407
+ record.violation = violation;
2408
+
2409
+ const instances = [];
2410
+ instances.push(instance);
2411
+ record.instances = instances;
2412
+
2413
+ records[bestPracticeIdString] = record;
2414
+ } else {
2415
+ const instances = record.instances;
2416
+ instances.push(instance);
2417
+ }
2418
+ }
2419
+
2420
+ return records;
2421
+ }
2422
+ }
2423
+
2424
+ /**
2425
+ * Defines supported strategies with which to create new reports and edit existing ones.
2426
+ * Choosing the correct report management strategy to meet your business objectives is critical to using Continuum's AMP reporting functionality correctly, so please consult our support documentation for more information.
2427
+ *
2428
+ * @readonly
2429
+ * @namespace
2430
+ * @type {object}
2431
+ */
2432
+ const ReportManagementStrategy = Object.freeze({
2433
+ /**
2434
+ * Append any new modules to a report, creating the report first if it doesn't already exist; do not overwrite any existing reports.
2435
+ * Useful for intentionally adding to an existing report, e.g. one that was just created recently, rather than creating a new report.
2436
+ *
2437
+ * @type {string}
2438
+ * @alias APPEND
2439
+ * @memberof! ReportManagementStrategy#
2440
+ */
2441
+ APPEND: "APPEND",
2442
+
2443
+ /**
2444
+ * Overwrite existing reports when a report with the same ID or name already exists, deleting any existing modules in the report in AMP prior to repopulating it with any new modules.
2445
+ * This is the recommended strategy for a more manual report generation workflow, e.g. when a developer is creating new reports from their own workstation, or when there is otherwise little reason to retain old reports.
2446
+ * While using this report management strategy, make sure to specify active modules by name (via {@link AMPReportingService#setActiveModuleByName}) rather than by ID (via {@link AMPReportingService#setActiveModuleById}); any modules in the active report—including the active module, assuming it's in the active report—will be deleted from AMP next time {@link AMPReportingService#submitAccessibilityConcernsToAMP} is invoked, making the active module ID invalid before any test results can be submitted to the module in AMP it used to reference, which will cause Continuum to throw a {@link NotFoundException}.
2447
+ *
2448
+ * @type {string}
2449
+ * @alias OVERWRITE
2450
+ * @memberof! ReportManagementStrategy#
2451
+ */
2452
+ OVERWRITE: "OVERWRITE",
2453
+
2454
+ /**
2455
+ * Always create new reports, guaranteeing uniqueness by appending the current date and time as an ISO 8601 timestamp to the end of each report's name; do not overwrite or append modules to any existing reports.
2456
+ * This is the recommended strategy for a continuous integration (CI) workflow, i.e. for a report generation process that's automatically performed periodically, or when you otherwise don't wish to overwrite any previous reports for record keeping purposes.
2457
+ *
2458
+ * @type {string}
2459
+ * @alias UNIQUE
2460
+ * @memberof! ReportManagementStrategy#
2461
+ */
2462
+ UNIQUE: "UNIQUE"
2463
+ });
2464
+
2465
+ /**
2466
+ * Defines supported strategies with which to create new modules and edit existing ones.
2467
+ * Choosing the correct module management strategy to meet your business objectives is critical to using Continuum's AMP reporting functionality correctly, so please consult our support documentation for more information.
2468
+ *
2469
+ * @readonly
2470
+ * @namespace
2471
+ * @type {object}
2472
+ */
2473
+ const ModuleManagementStrategy = Object.freeze({
2474
+ /**
2475
+ * Append any new accessibility concerns to a module, creating the module first if it doesn't already exist; do not overwrite any existing modules.
2476
+ * Useful for intentionally adding to an existing module, e.g. one that was just created recently, rather than creating a new module.
2477
+ *
2478
+ * @type {string}
2479
+ * @alias APPEND
2480
+ * @memberof! ModuleManagementStrategy#
2481
+ */
2482
+ APPEND: "APPEND",
2483
+
2484
+ /**
2485
+ * Overwrite existing modules when a module with the same ID or name already exists, deleting any existing accessibility concerns in the module prior to repopulating it with any new accessibility concerns.
2486
+ *
2487
+ * @type {string}
2488
+ * @alias OVERWRITE
2489
+ * @memberof! ModuleManagementStrategy#
2490
+ */
2491
+ OVERWRITE: "OVERWRITE",
2492
+
2493
+ /**
2494
+ * Don't report to AMP when a module with the same ID or name already exists; do not overwrite or append accessibility concerns to the existing module.
2495
+ *
2496
+ * @type {string}
2497
+ * @alias ABORT
2498
+ * @memberof! ModuleManagementStrategy#
2499
+ */
2500
+ ABORT: "ABORT"
2501
+ });
2502
+
2503
+ /**
2504
+ * This class encapsulates all the metadata relevant to an AMP report.
2505
+ *
2506
+ * @hideconstructor
2507
+ */
2508
+ class Report {
2509
+
2510
+ /**
2511
+ * @constructor
2512
+ * @returns {Report}
2513
+ */
2514
+ constructor(id, name) {
2515
+ this._id = id;
2516
+ this._name = name;
2517
+ }
2518
+
2519
+ /**
2520
+ * Gets the ID of this report.
2521
+ * This will be null if this report does not yet exist in AMP.
2522
+ *
2523
+ * @returns {?number} the ID of this report in AMP; null if this report does not yet exist in AMP
2524
+ */
2525
+ get id() {
2526
+ return this._id;
2527
+ }
2528
+
2529
+ /**
2530
+ * @private
2531
+ * @param id
2532
+ */
2533
+ set id(id) {
2534
+ this._id = id;
2535
+ }
2536
+
2537
+ /**
2538
+ * Gets the name of this report.
2539
+ *
2540
+ * @returns {string} the name of this report
2541
+ */
2542
+ get name() {
2543
+ return this._name;
2544
+ }
2545
+
2546
+ /**
2547
+ * @private
2548
+ * @param name
2549
+ */
2550
+ set name(name) {
2551
+ this._name = name;
2552
+ }
2553
+
2554
+ /**
2555
+ * Gets the URL to this report in AMP.
2556
+ *
2557
+ * @returns {?string} the URL to this report in AMP; null if this report does not yet exist in AMP
2558
+ */
2559
+ getAMPUrl() {
2560
+ return (this._id ? `${Configuration.getAmpInstanceUrl()}/public/reporting/view_report.php?report_id=${this._id}` : null);
2561
+ }
2562
+ }
2563
+
2564
+ /**
2565
+ * This class encapsulates all the metadata relevant to an AMP module.
2566
+ *
2567
+ * @hideconstructor
2568
+ */
2569
+ class Module {
2570
+
2571
+ /**
2572
+ * @constructor
2573
+ * @returns {Module}
2574
+ */
2575
+ constructor(id, name, location) {
2576
+ this._id = id;
2577
+ this._name = name;
2578
+ this._location = location;
2579
+ }
2580
+
2581
+ /**
2582
+ * Gets the ID of this module.
2583
+ * This will be null if this module does not yet exist in AMP.
2584
+ *
2585
+ * @returns {?number} the ID of this module in AMP; null if this module does not yet exist in AMP
2586
+ */
2587
+ get id() {
2588
+ return this._id;
2589
+ }
2590
+
2591
+ /**
2592
+ * @private
2593
+ * @param id
2594
+ */
2595
+ set id(id) {
2596
+ this._id = id;
2597
+ }
2598
+
2599
+ /**
2600
+ * Gets the name of this module.
2601
+ *
2602
+ * @returns {string} the name of this module
2603
+ */
2604
+ get name() {
2605
+ return this._name;
2606
+ }
2607
+
2608
+ /**
2609
+ * @private
2610
+ * @param name
2611
+ */
2612
+ set name(name) {
2613
+ this._name = name;
2614
+ }
2615
+
2616
+ /**
2617
+ * Gets the location of this module.
2618
+ *
2619
+ * @returns {string} the location of this module
2620
+ */
2621
+ get location() {
2622
+ return this._location;
2623
+ }
2624
+
2625
+ /**
2626
+ * @private
2627
+ * @param location
2628
+ */
2629
+ set location(location) {
2630
+ this._location = location;
2631
+ }
2632
+
2633
+ /**
2634
+ * Gets the URL to this module in AMP.
2635
+ *
2636
+ * @returns {?string} the URL to this module in AMP; null if this module does not yet exist in AMP
2637
+ */
2638
+ getAMPUrl() {
2639
+ return (this._id ? `${Configuration.getAmpInstanceUrl()}/public/reporting/view_module.php?module_id=${this._id}` : null);
2640
+ }
2641
+ }
2642
+
2643
+ /**
2644
+ * This class encapsulates shared utility functions shared across our other classes.
2645
+ *
2646
+ * @private
2647
+ */
2648
+ class PlatformUtil {
2649
+
2650
+ /**
2651
+ * (Heuristically) gets the name of the active platform runtime.
2652
+ * Returns null if the runtime can't be determined or isn't natively supported.
2653
+ *
2654
+ * @returns {?string}
2655
+ */
2656
+ static getRuntimeName() {
2657
+ if ((typeof require === 'function') && require('fs') && ('readFileSync' in require('fs'))) {
2658
+ return 'Node';
2659
+ } else {
2660
+ return null;
2661
+ }
2662
+ }
2663
+ }
2664
+
2665
+ /**
2666
+ * Signals that a method has been invoked at an illegal or inappropriate time.
2667
+ */
2668
+ class IllegalStateException extends Error {}
2669
+
2670
+ /**
2671
+ * Thrown to indicate that a method has been passed an illegal or inappropriate argument.
2672
+ */
2673
+ class IllegalArgumentException extends Error {}
2674
+
2675
+ /**
2676
+ * The class indicates a problem was encountered while connecting to a remote resource via HTTP/HTTPS.
2677
+ */
2678
+ class HttpErrorException extends Error {}
2679
+
2680
+ /**
2681
+ * The class indicates an expected entity could not be found.
2682
+ */
2683
+ class NotFoundException extends Error {}
2684
+
2685
+ /**
2686
+ * A global reference to Continuum.
2687
+ *
2688
+ * @const
2689
+ * @type {Continuum}
2690
+ */
2691
+ const continuum = new Continuum();
2692
+
2693
+ if (typeof module !== 'undefined') {
2694
+ module.exports.Continuum = continuum;
2695
+ module.exports.ReportManagementStrategy = ReportManagementStrategy;
2696
+ module.exports.ModuleManagementStrategy = ModuleManagementStrategy;
2697
+ }