doc-detective 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,688 @@
1
+ const yargs = require("yargs/yargs");
2
+ const { hideBin } = require("yargs/helpers");
3
+ const fs = require("fs");
4
+ const { exit } = require("process");
5
+ const path = require("path");
6
+ const uuid = require("uuid");
7
+ const nReadlines = require("n-readlines");
8
+ const { exec } = require("child_process");
9
+ const defaultConfig = require("../config.json");
10
+
11
+ exports.setArgs = setArgs;
12
+ exports.setConfig = setConfig;
13
+ exports.setFiles = setFiles;
14
+ exports.parseFiles = parseFiles;
15
+ exports.outputResults = outputResults;
16
+ exports.convertToGif = convertToGif;
17
+ exports.setEnvs = setEnvs;
18
+ exports.log = log;
19
+
20
+ const analyticsRequest =
21
+ "Thanks for using Doc Detective! If you want to contribute to the project, consider sending analytics to help us understand usage patterns and functional gaps. To turn on analytics, set 'analytics.send = true' in your config, or use the '-a true' argument. See https://github.com/hawkeyexl/doc-detective#analytics";
22
+ const defaultAnalyticsServers = [
23
+ {
24
+ name: "GA",
25
+ method: "post",
26
+ url: "https://www.google-analytics.com/mp/collect",
27
+ params: {
28
+ api_secret: "J_RJCtf0Rk-G42nX6XQBLQ",
29
+ measurement_id: "G-5VDP3TNPWC",
30
+ },
31
+ },
32
+ ];
33
+
34
+ // Define args
35
+ function setArgs(args) {
36
+ if (!args) return {};
37
+ let argv = yargs(hideBin(args))
38
+ .option("config", {
39
+ alias: "c",
40
+ description: "Path to a custom config file.",
41
+ type: "string",
42
+ })
43
+ .option("input", {
44
+ alias: "i",
45
+ description: "Path to a file or directory to parse for tests.",
46
+ type: "string",
47
+ })
48
+ .option("output", {
49
+ alias: "o",
50
+ description: "Path for a JSON file of test result output.",
51
+ type: "string",
52
+ })
53
+ .option("recursive", {
54
+ alias: "r",
55
+ description:
56
+ "Boolean. Recursively find test files in the test directory. Defaults to true.",
57
+ type: "string",
58
+ })
59
+ .option("ext", {
60
+ description:
61
+ "Comma-separated list of file extensions to test, including the leading period.",
62
+ type: "string",
63
+ })
64
+ .option("env", {
65
+ alias: "e",
66
+ description:
67
+ "Path to file of environment variables to set before running tests.",
68
+ type: "string",
69
+ })
70
+ .option("mediaDir", {
71
+ description: "Path to the media output directory.",
72
+ type: "string",
73
+ })
74
+ .option("browserHeadless", {
75
+ description:
76
+ "Boolean. Whether to run the browser in headless mode. Defaults to true.",
77
+ type: "string",
78
+ })
79
+ .option("browserPath", {
80
+ description:
81
+ "Path to a browser executable to run instead of puppeteer's bundled Chromium.",
82
+ type: "string",
83
+ })
84
+ .option("browserHeight", {
85
+ description:
86
+ "Height of the browser viewport in pixels. Default is 600 px.",
87
+ type: "number",
88
+ })
89
+ .option("browserWidth", {
90
+ description:
91
+ "Width of the browser viewport in pixels. Default is 800 px.",
92
+ type: "number",
93
+ })
94
+ .option("logLevel", {
95
+ alias: "l",
96
+ description:
97
+ "Detail level of logging events. Accepted values: silent, error, warning, info (default), debug",
98
+ type: "string",
99
+ })
100
+ .option("analytics", {
101
+ alias: "a",
102
+ description:
103
+ "Boolean. Defaults to false. Sends anonymous, aggregate analytics for usage and trend analysis. For details, see https://github.com/hawkeyexl/doc-detective#analytics.",
104
+ type: "string",
105
+ })
106
+ .option("analyticsUserId", {
107
+ description:
108
+ "Identifier of the organization or individual running tests.",
109
+ type: "string",
110
+ })
111
+ .option("analyticsDetailLevel", {
112
+ description:
113
+ "How much detail is included in the analytics object. Defaults to 'action'. Values: ['action', 'test', 'run']. For details, see https://github.com/hawkeyexl/doc-detective#analytics.",
114
+ type: "string",
115
+ })
116
+ .help()
117
+ .alias("help", "h").argv;
118
+
119
+ return argv;
120
+ }
121
+
122
+ function setLogLevel(config, argv) {
123
+ let logLevel = "";
124
+ let enums = ["debug", "info", "warning", "error", "silent"];
125
+ logLevel = argv.logLevel || process.env.DOC_LOG_LEVEL || config.logLevel || "info";
126
+ logLevel = String(logLevel).toLowerCase();
127
+ if (enums.indexOf(logLevel) >= 0) {
128
+ config.logLevel = logLevel;
129
+ log(config, "debug", `Log level set: ${logLevel}`);
130
+ } else {
131
+ config.logLevel = defaultConfig.logLevel;
132
+ log(
133
+ config,
134
+ "warning",
135
+ `Invalid log level. Reverted to default: ${config.logLevel}`
136
+ );
137
+ }
138
+ return config;
139
+ }
140
+
141
+ function selectConfig(config, argv) {
142
+ if (argv.config && fs.existsSync(argv.config)) {
143
+ // Argument
144
+ config = JSON.parse(fs.readFileSync(argv.config));
145
+ setLogLevel(config, argv);
146
+ log(config, "debug", "Loaded config from argument.");
147
+ } else if (
148
+ process.env.DOC_CONFIG_PATH &&
149
+ fs.existsSync(process.env.DOC_CONFIG_PATH)
150
+ ) {
151
+ // Env
152
+ config = JSON.parse(fs.readFileSync(process.env.DOC_CONFIG_PATH));
153
+ setLogLevel(config, argv);
154
+ log(config, "debug", "Loaded config from environment variable.");
155
+ } else if (JSON.stringify(config) != JSON.stringify({})) {
156
+ // Function param
157
+ config = config;
158
+ setLogLevel(config, argv);
159
+ log(config, "debug", "Loaded config from function parameter.");
160
+ } else {
161
+ // Default
162
+ config = defaultConfig;
163
+ setLogLevel(config, argv);
164
+ log(
165
+ config,
166
+ "warning",
167
+ "No custom config specified. Loaded default config."
168
+ );
169
+ }
170
+ return config;
171
+ }
172
+
173
+ function setEnv(config, argv) {
174
+ config.env = argv.env || process.env.DOC_ENV_PATH || config.env;
175
+ config.env = path.resolve(config.env);
176
+ if (config.env && fs.existsSync(config.env)) {
177
+ let envResult = setEnvs(config.env);
178
+ if (envResult.status === "PASS")
179
+ log(config, "debug", `Env file set: ${config.env}`);
180
+ if (envResult.status === "FAIL")
181
+ log(config, "warning", `File format issue. Can't load env file.`);
182
+ } else if (config.env && !fs.existsSync(config.env)) {
183
+ log(config, "warning", `Invalid file path. Can't load env file.`);
184
+ } else if (!config.env) {
185
+ log(config, "debug", "No env file specified.");
186
+ }
187
+ return config;
188
+ }
189
+
190
+ function setInput(config, argv) {
191
+ config.input = argv.input || process.env.DOC_INPUT_PATH || config.input;
192
+ config.input = path.resolve(config.input);
193
+ if (fs.existsSync(config.input)) {
194
+ log(config, "debug", `Input path set: ${config.input}`);
195
+ } else {
196
+ config.input = path.resolve(defaultConfig.input);
197
+ log(
198
+ config,
199
+ "warning",
200
+ `Invalid input path. Reverted to default: ${config.input}`
201
+ );
202
+ }
203
+ return config;
204
+ }
205
+
206
+ function setOutput(config, argv) {
207
+ config.output = argv.output || process.env.DOC_OUTPUT_PATH || config.output;
208
+ config.output = path.resolve(config.output);
209
+ log(config, "debug", `Output path set: ${config.output}`);
210
+ return config;
211
+ }
212
+
213
+ function setMediaDirectory(config, argv) {
214
+ config.mediaDirectory =
215
+ argv.mediaDir ||
216
+ process.env.DOC_MEDIA_DIRECTORY_PATH ||
217
+ config.mediaDirectory;
218
+ config.mediaDirectory = path.resolve(config.mediaDirectory);
219
+ if (fs.existsSync(config.mediaDirectory)) {
220
+ log(config, "debug", `Media directory set: ${config.mediaDirectory}`);
221
+ } else {
222
+ config.mediaDirectory = path.resolve(defaultConfig.mediaDirectory);
223
+ log(
224
+ config,
225
+ "warning",
226
+ `Invalid media directory. Reverted to default: ${config.mediaDirectory}`
227
+ );
228
+ }
229
+ return config;
230
+ }
231
+
232
+ function setRecursion(config, argv) {
233
+ config.recursive =
234
+ argv.recursive || process.env.DOC_RECURSIVE || config.recursive;
235
+ switch (config.recursive) {
236
+ case true:
237
+ case "true":
238
+ config.recursive = true;
239
+ log(config, "debug", `Recursion set: ${config.recursive}.`);
240
+ break;
241
+ case false:
242
+ case "false":
243
+ config.recursive = false;
244
+ log(config, "debug", `Recursion set: ${config.recursive}.`);
245
+ break;
246
+ default:
247
+ config.recursive = defaultConfig.recursive;
248
+ log(
249
+ config,
250
+ "warning",
251
+ `Invalid recursion valie. Reverted to default: ${config.recursive}.`
252
+ );
253
+ }
254
+ return config;
255
+ }
256
+
257
+ function setTestFileExtensions(config, argv) {
258
+ config.testExtensions =
259
+ argv.ext || process.env.DOC_TEST_EXTENSTIONS || config.testExtensions;
260
+ if (typeof config.testExtensions === "string")
261
+ config.testExtensions = config.testExtensions
262
+ .replace(/\s+/g, "")
263
+ .split(",");
264
+ if (config.testExtensions.length > 0) {
265
+ log(
266
+ config,
267
+ "debug",
268
+ `Test file extensions set: ${JSON.stringify(config.testExtensions)}`
269
+ );
270
+ } else {
271
+ config.testExtensions = defaultConfig.testExtensions;
272
+ log(
273
+ config,
274
+ "debug",
275
+ `Invalid test file extension value(s). Reverted to default: ${JSON.stringify(
276
+ config.testExtensions
277
+ )}`
278
+ );
279
+ }
280
+ return config;
281
+ }
282
+
283
+ function setBrowserHeadless(config, argv) {
284
+ config.browserOptions.headless =
285
+ argv.browserHeadless ||
286
+ process.env.DOC_BROWSER_HEADLESS ||
287
+ config.browserOptions.headless;
288
+ switch (config.browserOptions.headless) {
289
+ case true:
290
+ case "true":
291
+ config.browserOptions.headless = true;
292
+ log(
293
+ config,
294
+ "debug",
295
+ `Browser headless set to: ${config.browserOptions.headless}`
296
+ );
297
+ break;
298
+ case false:
299
+ case "false":
300
+ config.browserOptions.headless = false;
301
+ log(
302
+ config,
303
+ "debug",
304
+ `Browser headless set to: ${config.browserOptions.headless}`
305
+ );
306
+ break;
307
+ default:
308
+ config.browserOptions.headless = defaultConfig.browserOptions.headless;
309
+ log(
310
+ config,
311
+ "warning",
312
+ `Invalid browser headless value. Reverted to default: ${config.browserOptions.headless}`
313
+ );
314
+ }
315
+ return config;
316
+ }
317
+
318
+ function setBrowserPath(config, argv) {
319
+ config.browserOptions.path =
320
+ argv.browserPath ||
321
+ process.env.DOC_BROWSER_PATH ||
322
+ config.browserOptions.path;
323
+ config.browserOptions.path = path.resolve(config.browserOptions.path);
324
+ if (fs.existsSync(config.browserOptions.path)) {
325
+ log(config, "debug", `Browser path set: ${config.browserOptions.path}`);
326
+ } else {
327
+ config.browserOptions.path = defaultConfig.browserOptions.path;
328
+ log(
329
+ config,
330
+ "warning",
331
+ `Invalid browser path. Reverted to default Chromium install.`
332
+ );
333
+ }
334
+ return config;
335
+ }
336
+
337
+ function setBrowserHeight(config, argv) {
338
+ config.browserOptions.height =
339
+ argv.browserHeight ||
340
+ process.env.DOC_BROWSER_HEIGHT ||
341
+ config.browserOptions.height;
342
+ if (typeof config.browserOptions.height === "string") {
343
+ try {
344
+ config.browserOptions.height = Number(config.browserOptions.height);
345
+ } catch {
346
+ config.browserOptions.height = defaultConfig.browserOptions.height;
347
+ log(
348
+ config,
349
+ "warning",
350
+ `Invalid browser height. Reverted to default: ${config.browserOptions.height}`
351
+ );
352
+ }
353
+ }
354
+ if (typeof config.browserOptions.height === "number") {
355
+ log(config, "debug", `Browser height set: ${config.browserOptions.height}`);
356
+ } else {
357
+ config.browserOptions.height = defaultConfig.browserOptions.height;
358
+ log(
359
+ config,
360
+ "warning",
361
+ `Invalid browser height. Reverted to default: ${config.browserOptions.height}`
362
+ );
363
+ }
364
+ return config;
365
+ }
366
+
367
+ function setBrowserWidth(config, argv) {
368
+ config.browserOptions.width =
369
+ argv.browserWidth ||
370
+ process.env.DOC_BROWSER_WIDTH ||
371
+ config.browserOptions.width;
372
+ if (typeof config.browserOptions.width === "string") {
373
+ try {
374
+ config.browserOptions.width = Number(config.browserOptions.width);
375
+ } catch {
376
+ config.browserOptions.width = defaultConfig.browserOptions.width;
377
+ log(
378
+ config,
379
+ "warning",
380
+ `Invalid browser width. Reverted to default: ${config.browserOptions.width}`
381
+ );
382
+ }
383
+ }
384
+ if (typeof config.browserOptions.width === "number") {
385
+ log(config, "debug", `Browser width set: ${config.browserOptions.width}`);
386
+ } else {
387
+ config.browserOptions.width = defaultConfig.browserOptions.width;
388
+ log(
389
+ config,
390
+ "warning",
391
+ `Invalid browser width. Reverted to default: ${config.browserOptions.width}`
392
+ );
393
+ }
394
+ return config;
395
+ }
396
+
397
+ function setAnalytics(config, argv) {
398
+ config.analytics.send =
399
+ argv.analytics || process.env.DOC_ANALYTICS || config.analytics.send;
400
+ switch (config.analytics.send) {
401
+ case true:
402
+ case "true":
403
+ config.analytics.send = true;
404
+ log(config, "debug", `Analytics set: ${config.analytics.send}`);
405
+ break;
406
+ case false:
407
+ case "false":
408
+ config.analytics.send = false;
409
+ log(config, "debug", `Analytics set: ${config.analytics.send}`);
410
+ log(config, "info", analyticsRequest);
411
+ default:
412
+ config.analytics.send = defaultConfig.analytics.send;
413
+ log(
414
+ config,
415
+ "warning",
416
+ `Invalid analytics value. Reverted to default: ${config.analytics.send}`
417
+ );
418
+ }
419
+ return config;
420
+ }
421
+
422
+ function setAnalyticsUserId(config, argv) {
423
+ config.analytics.userId =
424
+ argv.analyticsUserId ||
425
+ process.env.DOC_ANALYTICS_USER_ID ||
426
+ config.analytics.userId;
427
+ log(config, "debug", `Analytics user ID set: ${config.analytics.userId}`);
428
+ return config;
429
+ }
430
+
431
+ function setAnalyticsDetailLevel(config, argv) {
432
+ let enums = ["run", "test", "action-simple", "action-detailed"];
433
+ detailLevel =
434
+ argv.analyticsDetailLevel ||
435
+ process.env.DOC_ANALYTCS_DETAIL_LEVEL ||
436
+ config.analytics.detailLevel;
437
+ detailLevel = String(detailLevel).toLowerCase();
438
+ if (enums.indexOf(detailLevel) >= 0) {
439
+ config.analytics.detailLevel = detailLevel;
440
+ log(config, "debug", `Analytics detail level set: ${detailLevel}`);
441
+ } else {
442
+ config.analytics.detailLevel = defaultConfig.analytics.detailLevel;
443
+ log(
444
+ config,
445
+ "warning",
446
+ `Invalid analytics detail level. Reverted to default: ${config.analytics.detailLevel}`
447
+ );
448
+ }
449
+ return config;
450
+ }
451
+
452
+ function setAnalyticsServers(config, argv) {
453
+ // Note: No validation on server info. It's up to users to get this right.
454
+ if (config.analytics.customServers.length > 0) {
455
+ config.analytics.servers = defaultAnalyticsServers.concat(
456
+ config.analytics.customServers
457
+ );
458
+ } else {
459
+ config.analytics.servers = defaultAnalyticsServers;
460
+ }
461
+ return config;
462
+ }
463
+
464
+ function setConfig(config, argv) {
465
+ config = setLogLevel(config, argv);
466
+
467
+ config = selectConfig(config, argv);
468
+
469
+ config = setEnv(config, argv);
470
+
471
+ config = setInput(config, argv);
472
+
473
+ config = setOutput(config, argv);
474
+
475
+ config = setMediaDirectory(config, argv);
476
+
477
+ config = setRecursion(config, argv);
478
+
479
+ config = setTestFileExtensions(config, argv);
480
+
481
+ config = setBrowserHeadless(config, argv);
482
+
483
+ config = setBrowserPath(config, argv);
484
+
485
+ config = setBrowserHeight(config, argv);
486
+
487
+ config = setBrowserWidth(config, argv);
488
+
489
+ config = setAnalytics(config, argv);
490
+
491
+ config = setAnalyticsUserId(config, argv);
492
+
493
+ config = setAnalyticsDetailLevel(config, argv);
494
+
495
+ config = setAnalyticsServers(config, argv);
496
+
497
+ return config;
498
+ }
499
+
500
+ // Set array of test files
501
+ function setFiles(config) {
502
+ let dirs = [];
503
+ let files = [];
504
+
505
+ // Validate input
506
+ const input = path.resolve(config.input);
507
+ let isFile = fs.statSync(input).isFile();
508
+ let isDir = fs.statSync(input).isDirectory();
509
+ if (!isFile && !isDir) {
510
+ log(config, "error", "Input isn't a valid file or directory.");
511
+ exit(1);
512
+ }
513
+
514
+ // Parse input
515
+ if (isFile) {
516
+ // if single file specified
517
+ files[0] = input;
518
+ return files;
519
+ } else if (isDir) {
520
+ // Load files from drectory
521
+ dirs[0] = input;
522
+ for (let i = 0; i < dirs.length; i++) {
523
+ fs.readdirSync(dirs[i]).forEach((object) => {
524
+ let content = path.resolve(dirs[i] + "/" + object);
525
+ let isFile = fs.statSync(content).isFile();
526
+ let isDir = fs.statSync(content).isDirectory();
527
+ if (isFile) {
528
+ // is a file
529
+ if (
530
+ // No specified extension filter list, or file extension is present in extension filter list.
531
+ config.testExtensions === "" ||
532
+ config.testExtensions.includes(path.extname(content))
533
+ ) {
534
+ files.push(content);
535
+ }
536
+ } else if (isDir) {
537
+ // is a directory
538
+ if (config.recursive) {
539
+ // recursive set to true
540
+ dirs.push(content);
541
+ }
542
+ }
543
+ });
544
+ }
545
+ return files;
546
+ }
547
+ }
548
+
549
+ // Parse files for tests
550
+ function parseFiles(config, files) {
551
+ let json = { tests: [] };
552
+
553
+ // Loop through test files
554
+ files.forEach((file) => {
555
+ log(config, "debug", `file: ${file}`);
556
+ let id = uuid.v4();
557
+ let line;
558
+ let lineNumber = 1;
559
+ let inputFile = new nReadlines(file);
560
+ let extension = path.extname(file);
561
+ let fileType = config.fileTypes.find((fileType) =>
562
+ fileType.extensions.includes(extension)
563
+ );
564
+ if (!fileType && extension !== ".json") {
565
+ // Missing filetype options
566
+ console.log(
567
+ `Error: Specify options for the ${extension} extension in your config file.`
568
+ );
569
+ exit(1);
570
+ }
571
+
572
+ // If file is JSON, add tests straight to array
573
+ if (path.extname(file) === ".json") {
574
+ content = require(file);
575
+ content.tests.forEach((test) => {
576
+ json.tests.push(test);
577
+ });
578
+ } else {
579
+ // Loop through lines
580
+ while ((line = inputFile.next())) {
581
+ let lineJson = "";
582
+ let subStart = "";
583
+ let subEnd = "";
584
+ if (line.includes(fileType.openTestStatement)) {
585
+ const lineAscii = line.toString("ascii");
586
+ if (fileType.closeTestStatement) {
587
+ subEnd = lineAscii.lastIndexOf(fileType.closeTestStatement);
588
+ } else {
589
+ subEnd = lineAscii.length;
590
+ }
591
+ subStart =
592
+ lineAscii.indexOf(fileType.openTestStatement) +
593
+ fileType.openTestStatement.length;
594
+ lineJson = JSON.parse(lineAscii.substring(subStart, subEnd));
595
+ if (!lineJson.testId) {
596
+ lineJson.testId = id;
597
+ }
598
+ let test = json.tests.find((item) => item.id === lineJson.testId);
599
+ if (!test) {
600
+ json.tests.push({ id: lineJson.testId, file, actions: [] });
601
+ test = json.tests.find((item) => item.id === lineJson.testId);
602
+ }
603
+ delete lineJson.testId;
604
+ lineJson.line = lineNumber;
605
+ test.actions.push(lineJson);
606
+ }
607
+ lineNumber++;
608
+ }
609
+ }
610
+ });
611
+ return json;
612
+ }
613
+
614
+ async function outputResults(config, results) {
615
+ let data = JSON.stringify(results, null, 2);
616
+ fs.writeFile(config.output, data, (err) => {
617
+ if (err) throw err;
618
+ });
619
+ }
620
+
621
+ async function convertToGif(config, input, fps, width) {
622
+ if (!fs.existsSync(input)) return { error: "Invalid input." };
623
+ let output = path.join(
624
+ path.parse(input).dir,
625
+ path.parse(input).name + ".gif"
626
+ );
627
+ if (!fps) fps = 15;
628
+ let command = `ffmpeg -nostats -loglevel 0 -y -i ${input} -vf "fps=${fps},scale=${width}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 ${output}`;
629
+ exec(command, (error, stdout, stderr) => {
630
+ if (error) {
631
+ log(config, "debug", error.message);
632
+ return { error: error.message };
633
+ }
634
+ if (stderr) {
635
+ log(config, "debug", stderr);
636
+ return { stderr };
637
+ }
638
+ log(config, "debug", stdout);
639
+ return { stdout };
640
+ });
641
+ return output;
642
+ }
643
+
644
+ async function setEnvs(envsFile) {
645
+ const fileExists = fs.existsSync(envsFile);
646
+ if (fileExists) {
647
+ require("dotenv").config({ path: envsFile, override: true });
648
+ return { status: "PASS", description: "Envs set." };
649
+ } else {
650
+ return { status: "FAIL", description: "Invalid file." };
651
+ }
652
+ }
653
+
654
+ async function log(config, level, message) {
655
+ let logLevelMatch = false;
656
+ if (config.logLevel === "error" && level === "error") {
657
+ logLevelMatch = true;
658
+ } else if (
659
+ config.logLevel === "warning" &&
660
+ (level === "error" || level === "warning")
661
+ ) {
662
+ logLevelMatch = true;
663
+ } else if (
664
+ config.logLevel === "info" &&
665
+ (level === "error" || level === "warning" || level === "info")
666
+ ) {
667
+ logLevelMatch = true;
668
+ } else if (
669
+ config.logLevel === "debug" &&
670
+ (level === "error" ||
671
+ level === "warning" ||
672
+ level === "info" ||
673
+ level === "debug")
674
+ ) {
675
+ logLevelMatch = true;
676
+ }
677
+
678
+ if (logLevelMatch) {
679
+ if (typeof message === "string") {
680
+ let logMessage = `(${level.toUpperCase()}) ${message}`;
681
+ console.log(logMessage);
682
+ } else if (typeof message === "object") {
683
+ let logMessage = `(${level.toUpperCase()})`;
684
+ console.log(logMessage);
685
+ console.log(message);
686
+ }
687
+ }
688
+ }