spectre-reporter-html 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1171 @@
1
+ require 'cgi'
2
+ require 'base64'
3
+
4
+ module Spectre
5
+ module Reporter
6
+ class HTML
7
+ VERSION = '1.0.0'
8
+
9
+ def initialize config
10
+ @config = config
11
+ @date_format = '%FT%T.%L'
12
+ end
13
+
14
+ def get_error_info error
15
+ non_spectre_files = error.backtrace.select { |x| !x.include? 'lib/spectre' }
16
+
17
+ if non_spectre_files.count > 0
18
+ causing_file = non_spectre_files.first
19
+ else
20
+ causing_file = error.backtrace[0]
21
+ end
22
+
23
+ matches = causing_file.match(/(.*\.rb):(\d+)/)
24
+
25
+ return [nil, nil] unless matches
26
+
27
+ file, line = matches.captures
28
+ file.slice!(Dir.pwd + '/')
29
+
30
+ return [file, line]
31
+ end
32
+
33
+ def read_resource filename
34
+ file_path = File.join(__dir__, '../../../resources/', filename)
35
+
36
+ File.open(file_path, 'rb') do |file|
37
+ return file.read
38
+ end
39
+ end
40
+
41
+ def report run_infos
42
+ now = Time.now
43
+
44
+ failures = run_infos.select { |x| x.failure != nil }
45
+ errors = run_infos.select { |x| x.error != nil }
46
+ skipped = run_infos.select { |x| x.skipped? }
47
+ succeeded_count = run_infos.count - failures.count - errors.count - skipped.count
48
+
49
+ if failures.count > 0
50
+ overall_status = 'failed'
51
+ elsif errors.count > 0
52
+ overall_status = 'error'
53
+ elsif skipped.count > 0
54
+ overall_status = 'skipped'
55
+ else
56
+ overall_status = 'success'
57
+ end
58
+
59
+ json_report = {
60
+ command: $COMMAND,
61
+ project: @config['project'],
62
+ date: now.strftime(@date_format),
63
+ environment: @config['environment'],
64
+ hostname: Socket.gethostname,
65
+ duration: run_infos.sum { |x| x.duration },
66
+ failures: failures.count,
67
+ errors: errors.count,
68
+ skipped: skipped.count,
69
+ succeeded: succeeded_count,
70
+ total: run_infos.count,
71
+ overall_status: overall_status,
72
+ tags: run_infos
73
+ .map { |x| x.spec.tags }
74
+ .flatten
75
+ .uniq
76
+ .sort,
77
+ run_infos: run_infos.map do |run_info|
78
+ failure = nil
79
+ error = nil
80
+
81
+ if run_info.failed? and not run_info.failure.cause
82
+ failure_message = "Expected #{run_info.failure.expectation}"
83
+ failure_message += " with #{run_info.data}" if run_info.data
84
+ failure_message += " but it failed"
85
+ failure_message += " with message: #{run_info.failure.message}" if run_info.failure.message
86
+
87
+ failure = {
88
+ message: failure_message,
89
+ expected: run_info.failure.expected,
90
+ actual: run_info.failure.actual,
91
+ }
92
+ end
93
+
94
+ if run_info.error or (run_info.failed? and run_info.failure.cause)
95
+ error = run_info.error || run_info.failure.cause
96
+
97
+ file, line = get_error_info(error)
98
+
99
+ error = {
100
+ type: error.class.name,
101
+ message: error.message,
102
+ file: file,
103
+ line: line,
104
+ stack_trace: error.backtrace,
105
+ }
106
+ end
107
+
108
+ {
109
+ status: run_info.status,
110
+ subject: run_info.spec.subject.desc,
111
+ context: run_info.spec.context.__desc,
112
+ tags: run_info.spec.tags,
113
+ name: run_info.spec.name,
114
+ desc: run_info.spec.desc,
115
+ file: run_info.spec.file,
116
+ started: run_info.started.strftime(@date_format),
117
+ finished: run_info.finished.strftime(@date_format),
118
+ duration: run_info.duration,
119
+ properties: run_info.properties,
120
+ data: run_info.data,
121
+ failure: failure,
122
+ error: error,
123
+ # the <script> element has to be escaped in any string, as it causes the inline JavaScript to break
124
+ log: run_info.log.map { |x| [x[0], x[1].to_s.gsub(/\<(\/*script)/, '<`\1'), x[2], x[3]] },
125
+ }
126
+ end,
127
+ config: @config.obfuscate!,
128
+ }
129
+
130
+ vuejs_content = read_resource('vue.global.prod.js')
131
+ open_sans_font = Base64.strict_encode64 read_resource('OpenSans-Regular.ttf')
132
+ fa_solid = Base64.strict_encode64 read_resource('fa-solid-900.ttf')
133
+ fa_regular = Base64.strict_encode64 read_resource('fa-regular-400.ttf')
134
+ icon = read_resource('spectre_icon.svg')
135
+
136
+ html_str = <<~HTML
137
+ <html>
138
+ <head>
139
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
140
+ <title>Spectre Report</title>
141
+
142
+ <!-- https://unpkg.com/vue@3.2.29/dist/vue.global.prod.js -->
143
+ <script>#{vuejs_content}</script>
144
+
145
+ <style>
146
+ @font-face{
147
+ font-family: 'Open Sans Regular';
148
+ src: url(data:font/ttf;base64,#{open_sans_font}) format('truetype');
149
+ }
150
+
151
+ @font-face{
152
+ font-family: 'Font Awesome';
153
+ font-weight: 900;
154
+ src: url(data:font/ttf;base64,#{fa_solid}) format('truetype');
155
+ }
156
+
157
+ @font-face{
158
+ font-family: 'Font Awesome';
159
+ font-weight: 400;
160
+ src: url(data:font/ttf;base64,#{fa_regular}) format('truetype');
161
+ }
162
+
163
+ * {
164
+ box-sizing: border-box;
165
+ font-weight: inherit;
166
+ line-height: 1.5em;
167
+ font-size: inherit;
168
+ margin: 0rem;
169
+ padding: 0rem;
170
+ }
171
+
172
+ html, body {
173
+ padding: 0rem;
174
+ margin: 0rem;
175
+ }
176
+
177
+ body {
178
+ font-family: 'Open Sans Regular', Arial, sans-serif;
179
+ color: #223857;
180
+ font-size: 16px;
181
+ background-color: #f6f7f8;
182
+ }
183
+
184
+ section {
185
+ margin-bottom: 2rem;
186
+ }
187
+
188
+ #spectre-banner {
189
+ padding: 1rem;
190
+ font-weight: bold;
191
+ text-align: center;
192
+ text-transform: uppercase;
193
+ }
194
+
195
+ #spectre-report {
196
+ padding: 2rem;
197
+ width: 90%;
198
+ margin: auto;
199
+ }
200
+
201
+ #spectre-tags {
202
+ text-align: center;
203
+ }
204
+
205
+ #spectre-logo {
206
+ display: block;
207
+ margin: 1.5rem auto;
208
+ }
209
+
210
+ fieldset {
211
+ border: 1px solid #dfe2e7;
212
+ border-radius: 3px;
213
+ margin-bottom: 0.3rem;
214
+ width: 100%;
215
+ padding: 0rem 1rem 1rem 1rem;
216
+ }
217
+
218
+ legend {
219
+ color: #bcc5d1;
220
+ font-size: 0.9rem;
221
+ padding: 0.3em;
222
+ }
223
+
224
+ th {
225
+ font-weight: bold;
226
+ text-align: right;
227
+ }
228
+
229
+ td {
230
+ padding: 0.2em;
231
+ }
232
+
233
+ ul {
234
+ padding: 0rem 0rem 0rem 1rem;
235
+ }
236
+
237
+ /* spectre-logo */
238
+
239
+ .spectre-status-success .spectre-logo-eye {
240
+ fill: #9ddf1c !important;
241
+ }
242
+
243
+ .spectre-status-failed .spectre-logo-eye {
244
+ fill: #e61160 !important;
245
+ }
246
+
247
+ .spectre-status-error .spectre-logo-eye {
248
+ fill: #f5d915 !important;
249
+ }
250
+
251
+ .spectre-status-skipped .spectre-logo-eye {
252
+ fill: #c1d5d9 !important;
253
+ }
254
+
255
+ /* spectre-controls */
256
+
257
+ ul.spectre-controls {
258
+ list-style: none;
259
+ margin: 0rem;
260
+ padding: 0rem;
261
+ text-align: center;
262
+ }
263
+
264
+ ul.spectre-controls > li {
265
+ display: inline;
266
+ margin-right: 1rem;
267
+ }
268
+
269
+ .spectre-controls-clear {
270
+ position: relative;
271
+ }
272
+
273
+ .spectre-controls-clear:before {
274
+ font-family: 'Font Awesome';
275
+ font-weight: 900;
276
+ content: '\\f0b0';
277
+ }
278
+
279
+ .spectre-controls-clear.active:before {
280
+ font-family: 'Font Awesome';
281
+ font-weight: 900;
282
+ content: '\\e17b';
283
+ }
284
+
285
+ .spectre-filter-count {
286
+ font-size: 0.7em;
287
+ background-color: #223857;
288
+ color: #fff;
289
+ border-radius: 999px;
290
+ padding: 0em 0.5em;
291
+ position: absolute;
292
+ top: -1em;
293
+ }
294
+
295
+ .spectre-link {
296
+ display: inline;
297
+ border-bottom: 2px solid #3196d6;
298
+ text-decoration: none;
299
+ cursor: pointer;
300
+ }
301
+
302
+ .spectre-link:hover {
303
+ background-color: #dbedf8;
304
+ }
305
+
306
+ legend.spectre-toggle {
307
+ cursor: pointer;
308
+ user-select: none;
309
+ }
310
+
311
+ legend.spectre-toggle:before {
312
+ content: '+';
313
+ margin-right: 0.2em;
314
+ }
315
+
316
+ .spectre-expander:before {
317
+ font-family: 'Font Awesome';
318
+ font-weight: 400;
319
+ display: inline-block;
320
+ cursor: pointer;
321
+ content: '\\f0fe';
322
+ margin-right: 0.5rem;
323
+ }
324
+
325
+ .active > .spectre-expander:before {
326
+ content: '\\f146';
327
+ }
328
+
329
+ #spectre-environment,
330
+ .spectre-command {
331
+ display: block;
332
+ font-family: monospace;
333
+ background-color: #223857;
334
+ color: #fff;
335
+ border-radius: 3px;
336
+ padding: 0.5rem;
337
+ }
338
+
339
+ .spectre-command:before {
340
+ content: '$';
341
+ margin-right: 0.5rem;
342
+ }
343
+
344
+ /* spectre-summary */
345
+
346
+ .spectre-summary {
347
+ text-align: center;
348
+ }
349
+
350
+ .spectre-summary span {
351
+ font-size: 1.5rem;
352
+ }
353
+
354
+ .spectre-summary span:not(:last-child) {
355
+ margin-right: 2em;
356
+ }
357
+
358
+ span.spectre-summary-project {
359
+ display: block;
360
+ font-size: 2.5em;
361
+ text-transform: uppercase;
362
+ margin: 1rem 0rem !important;
363
+ }
364
+
365
+ .spectre-summary-environment {
366
+ font-family: monospace;
367
+ }
368
+
369
+ .spectre-summary-environment:before,
370
+ .spectre-summary-date:before,
371
+ .spectre-summary-duration:before,
372
+ .spectre-summary-host:before {
373
+ font-family: 'Font Awesome';
374
+ margin-right: 0.5em;
375
+ }
376
+
377
+ .spectre-summary-environment:before {
378
+ font-weight: 900;
379
+ content: '\\e587';
380
+ }
381
+
382
+ .spectre-summary-date:before {
383
+ content: '\\f133';
384
+ }
385
+
386
+ .spectre-summary-duration:before {
387
+ font-weight: 900;
388
+ content: '\\f2f2';
389
+ }
390
+
391
+ .spectre-summary-host:before {
392
+ font-weight: 900;
393
+ content: '\\e4e5';
394
+ }
395
+
396
+ ul.spectre-summary-result {
397
+ list-style: none;
398
+ padding: 0rem;
399
+ margin: 0rem;
400
+ text-align: center;
401
+ }
402
+
403
+ ul.spectre-summary-result > li {
404
+ display: inline;
405
+ margin-right: 1rem;
406
+ }
407
+
408
+ ul.spectre-tags {
409
+ list-style: none;
410
+ padding: 0rem;
411
+ margin: 0rem;
412
+ user-select: none;
413
+ }
414
+
415
+ ul.spectre-tags > li {
416
+ display: inline-block;
417
+ line-height: 1.5rem;
418
+ cursor: pointer;
419
+ }
420
+
421
+ .spectre-tag {
422
+ color: #11c7e6;
423
+ padding: 0rem 0.5rem;
424
+ }
425
+
426
+ .spectre-tag:before {
427
+ content: '#';
428
+ }
429
+
430
+ .spectre-tag.active {
431
+ background-color: #11c7e6;
432
+ color: #223857;
433
+ border-radius: 999px;
434
+ }
435
+
436
+ /* spectre-button */
437
+
438
+ .spectre-button {
439
+ background-color: #11c7e6;
440
+ border: 3px solid #11c7e6;
441
+ color: #0b2a63;
442
+ cursor: pointer;
443
+ border-radius: 999px;
444
+ padding: 0.5rem 1rem;
445
+ font-weight: bold;
446
+ user-select: none;
447
+ }
448
+
449
+ .spectre-button:hover {
450
+ background-color: #7fe4f6;
451
+ border-color: #7fe4f6;
452
+ }
453
+
454
+ .spectre-button:active {
455
+ background-color: #d2f6fc;
456
+ border-color: #d2f6fc;
457
+ }
458
+
459
+ .spectre-button.disabled {
460
+ background: none !important;
461
+ border-color: rgba(0, 0, 0, 0.1) !important;
462
+ color: rgba(0, 0, 0, 0.175) !important;
463
+ cursor: default !important;
464
+ }
465
+
466
+ .spectre-button.inactive {
467
+ background: none;
468
+ border-color: #0b2a63;
469
+ }
470
+
471
+ .spectre-button.inactive:hover {
472
+ background-color: #0b2a63;
473
+ border-color: #0b2a63;
474
+ color: #fff;
475
+ }
476
+
477
+ .spectre-button.inactive:active {
478
+ background-color: #0b3476;
479
+ border-color: #0b3476;
480
+ color: #fff;
481
+ }
482
+
483
+ /* spectre-badge */
484
+
485
+ .spectre-badge {
486
+ font-size: 0.8em;
487
+ font-weight: bold;
488
+ background-color: #11c7e6;
489
+ color: #0b2a63;
490
+ border-radius: 999rem;
491
+ padding: 0.2rem 0.8rem;
492
+ }
493
+
494
+ /* spectre-result */
495
+
496
+ .spectre-result-subjects {
497
+ list-style: none;
498
+ padding: 3rem;
499
+ margin: 0rem;
500
+ background-color: #fff;
501
+ border-radius: 3px;
502
+ box-shadow: 0 2px 5px 0 rgb(0 0 0 / 20%);
503
+ }
504
+
505
+ .spectre-result-runinfos {
506
+ display: none;
507
+ }
508
+
509
+ .active > .spectre-result-runinfos {
510
+ display: block;
511
+ }
512
+
513
+ ul.spectre-result-contexts, ul.spectre-result-runinfos {
514
+ list-style: none;
515
+ margin: 0rem;
516
+ }
517
+
518
+ .spectre-result-subjects > li {
519
+ margin-bottom: 1rem;
520
+ }
521
+
522
+ /* spectre-subject */
523
+
524
+ .spectre-subject-desc {
525
+ font-weight: bold;
526
+ font-size: 1.3rem;
527
+ }
528
+
529
+ /* spectre-context */
530
+
531
+ .spectre-context-desc {
532
+ font-style: italic;
533
+ color: #11c7e6;
534
+ }
535
+
536
+ .spectre-context {
537
+ line-height: 1.5em;
538
+ }
539
+
540
+ /* spectre-runinfo */
541
+
542
+ .spectre-runinfo {
543
+ padding: 0.8rem 1rem;
544
+ }
545
+
546
+ .spectre-runinfo:not(:last-child) {
547
+ border-bottom: 1px solid rgb(0, 0, 0, 0.1);
548
+ }
549
+
550
+ .spectre-runinfo-description > span {
551
+ margin: 0em 0.2em;
552
+ }
553
+
554
+ .spectre-runinfo-details {
555
+ margin: 1rem 0rem;
556
+ padding-left: 1rem;
557
+ display: none;
558
+ }
559
+
560
+ .spectre-runinfo.active .spectre-runinfo-details {
561
+ display: block;
562
+ }
563
+
564
+ .spectre-description-data {
565
+ color: #11c7e6;
566
+ }
567
+
568
+ .spectre-description-name {
569
+ color: #9aa7b9;
570
+ }
571
+
572
+ .spectre-description-data:before,
573
+ .spectre-description-name:before {
574
+ content: '[';
575
+ }
576
+
577
+ .spectre-description-data:after,
578
+ .spectre-description-name:after {
579
+ content: ']';
580
+ }
581
+
582
+ .spectre-file {
583
+ font-family: monospace;
584
+ }
585
+
586
+ .spectre-code {
587
+ font-family: monospace;
588
+ }
589
+
590
+ .spectre-date {
591
+ font-style: italic;
592
+ }
593
+
594
+ /* spectre-details */
595
+
596
+ .spectre-details-status {
597
+ text-transform: uppercase;
598
+ }
599
+
600
+ /* spectre icons */
601
+
602
+ .spectre-description-status:before {
603
+ font-family: 'Font Awesome';
604
+ font-weight: 900;
605
+ margin: 0em 0.3em;
606
+ }
607
+
608
+ .spectre-runinfo.spectre-status-success .spectre-description-status:before {
609
+ content: '\\f00c';
610
+ color: #9ddf1c;
611
+ }
612
+
613
+ .spectre-runinfo.spectre-status-failed .spectre-description-status:before {
614
+ content: '\\f7a9';
615
+ color: #e61160;
616
+ }
617
+
618
+ .spectre-runinfo.spectre-status-error .spectre-description-status:before {
619
+ content: '\\f057';
620
+ font-weight: 400;
621
+ color: #f5d915;
622
+ }
623
+
624
+ .spectre-runinfo.spectre-status-skipped .spectre-description-status:before {
625
+ content: '\\f04e';
626
+ color: #c1d5d9;
627
+ }
628
+
629
+ /* spectre-status colors */
630
+
631
+ /* spectre-status colors SUCCESS */
632
+
633
+ .spectre-runinfo.spectre-status-success .spectre-details-status,
634
+ .spectre-button.spectre-summary-succeeded,
635
+ .spectre-status-success #spectre-banner {
636
+ background-color: #9ddf1c;
637
+ border-color: #9ddf1c;
638
+ }
639
+
640
+ .spectre-button.spectre-summary-succeeded:hover {
641
+ background-color: #c1f55b;
642
+ border-color: #c1f55b;
643
+ color: #0b2a63;
644
+ }
645
+
646
+ .spectre-button.spectre-summary-succeeded:active {
647
+ background-color: #d3ff7c;
648
+ border-color: #d3ff7c;
649
+ color: #0b2a63;
650
+ }
651
+
652
+ .spectre-button.inactive.spectre-summary-succeeded {
653
+ background: none;
654
+ border-color: #9ddf1c;
655
+ color: #0b2a63;
656
+ }
657
+
658
+ .spectre-button.inactive.spectre-summary-succeeded:hover {
659
+ background-color: #9ddf1c;
660
+ border-color: #9ddf1c;
661
+ }
662
+
663
+ .spectre-button.inactive.spectre-summary-succeeded:active {
664
+ background-color: #83bd11;
665
+ border-color: #83bd11;
666
+ }
667
+
668
+ .spectre-log-level-info {
669
+ color: #9ddf1c;
670
+ }
671
+
672
+ /* spectre-status colors FAILED */
673
+
674
+ .spectre-runinfo.spectre-status-failed .spectre-details-status,
675
+ .spectre-button.spectre-summary-failures,
676
+ .spectre-status-failed #spectre-banner {
677
+ background-color: #e61160;
678
+ border-color: #e61160;
679
+ }
680
+
681
+ .spectre-button.spectre-summary-failures:hover {
682
+ background-color: #f56198;
683
+ border-color: #f56198;
684
+ color: #0b2a63;
685
+ }
686
+
687
+ .spectre-button.spectre-summary-failures:active {
688
+ background-color: #ffadcb;
689
+ border-color: #ffadcb;
690
+ color: #0b2a63;
691
+ }
692
+
693
+ .spectre-button.inactive.spectre-summary-failures {
694
+ background: none;
695
+ border-color: #e61160;
696
+ color: #0b2a63;
697
+ }
698
+
699
+ .spectre-button.inactive.spectre-summary-failures:hover {
700
+ background-color: #e61160;
701
+ border-color: #e61160;
702
+ }
703
+
704
+ .spectre-button.inactive.spectre-summary-failures:active {
705
+ background-color: #bb084a;
706
+ border-color: #bb084a;
707
+ }
708
+
709
+ .spectre-log-level-error {
710
+ color: #e61160;
711
+ }
712
+
713
+ /* spectre-status colors ERROR */
714
+
715
+ .spectre-runinfo.spectre-status-error .spectre-details-status,
716
+ .spectre-button.spectre-summary-errors,
717
+ .spectre-status-error #spectre-banner {
718
+ background-color: #f5d915;
719
+ border-color: #f5d915;
720
+ }
721
+
722
+ .spectre-button.spectre-summary-errors:hover {
723
+ background-color: #fde95e;
724
+ border-color: #fde95e;
725
+ color: #0b2a63;
726
+ }
727
+
728
+ .spectre-button.spectre-summary-errors:active {
729
+ background-color: #fff29b;
730
+ border-color: #fff29b;
731
+ color: #0b2a63;
732
+ }
733
+
734
+ .spectre-button.inactive.spectre-summary-errors {
735
+ background: none;
736
+ border-color: #f5d915;
737
+ color: #0b2a63;
738
+ }
739
+
740
+ .spectre-button.inactive.spectre-summary-errors:hover {
741
+ background-color: #f5d915;
742
+ border-color: #f5d915;
743
+ }
744
+
745
+ .spectre-button.inactive.spectre-summary-errors:active {
746
+ background-color: #e7ca00;
747
+ border-color: #e7ca00;
748
+ }
749
+
750
+ .spectre-log-level-warn {
751
+ color: #f5d915;
752
+ }
753
+
754
+ /* spectre-status colors SKIPPED */
755
+
756
+ .spectre-runinfo.spectre-status-skipped .spectre-details-status,
757
+ .spectre-button.spectre-summary-skipped,
758
+ .spectre-status-skipped #spectre-banner {
759
+ background-color: #c1d5d9;
760
+ border-color: #c1d5d9;
761
+ }
762
+
763
+ .spectre-log-level-debug {
764
+ color: #c1d5d9;
765
+ }
766
+
767
+ /* spectre-log */
768
+
769
+ .spectre-log {
770
+ font-family: monospace;
771
+ font-size: 0.8rem;
772
+ list-style: none;
773
+ padding: 0rem;
774
+ margin: 0rem;
775
+ }
776
+
777
+ .spectre-log-entry {
778
+ display: block;
779
+ font-family: monospace;
780
+ white-space: pre;
781
+ }
782
+
783
+ .spectre-log-timestamp {
784
+ font-style: italic;
785
+ color: rgba(0, 0, 0, 0.5);
786
+ }
787
+
788
+ .spectre-log-timestamp:before {
789
+ content: '[';
790
+ color: #000;
791
+ }
792
+
793
+ .spectre-log-timestamp:after {
794
+ content: ']';
795
+ color: #000;
796
+ }
797
+
798
+ .spectre-log-level {
799
+ text-transform: uppercase;
800
+ }
801
+ </style>
802
+ </head>
803
+ <body>
804
+ <div id="app">
805
+ <div :class="'spectre-status-' + spectreReport.overall_status">
806
+ <div id="spectre-banner">{{ spectreReport.overall_status }}</div>
807
+
808
+ <div class="spectre-summary">
809
+ #{icon}
810
+
811
+ <span class="spectre-summary-project">{{ spectreReport.project }}</span>
812
+
813
+ <span class="spectre-summary-environment">{{ spectreReport.environment }}</span>
814
+ <span class="spectre-summary-date">{{ new Date(spectreReport.date).toLocaleString() }}</span>
815
+ <span class="spectre-summary-duration">{{ spectreReport.duration.toDurationString() }}</span>
816
+ <span class="spectre-summary-host">{{ spectreReport.hostname }}</span>
817
+ </div>
818
+
819
+ <div id="spectre-report">
820
+ <section>
821
+ <div class="spectre-command">{{ spectreCommand }}</div>
822
+ </section>
823
+
824
+ <section id="spectre-tags">
825
+ <ul class="spectre-tags">
826
+ <li class="spectre-tag" v-for="tag in spectreReport.tags" @click="toggleTagFilter(tag)" :class="{ active: tagFilter.includes(tag)}">{{ tag }}</li>
827
+ </ul>
828
+ </section>
829
+
830
+ <section>
831
+ <ul class="spectre-summary-result">
832
+ <li
833
+ class="spectre-button spectre-summary-succeeded"
834
+ :class="{ disabled: spectreReport.succeeded == 0, inactive: statusFilter != null && statusFilter != 'success' }"
835
+ @click="filter('success')">{{ spectreReport.succeeded }} succeeded</li>
836
+
837
+ <li
838
+ class="spectre-button spectre-summary-skipped"
839
+ :class="{ disabled: spectreReport.skipped == 0, inactive: statusFilter != null && statusFilter != 'skipped' }"
840
+ @click="filter('skipped')">{{ spectreReport.skipped }} skipped</li>
841
+
842
+ <li
843
+ class="spectre-button spectre-summary-failures"
844
+ :class="{ disabled: spectreReport.failures == 0, inactive: statusFilter != null && statusFilter != 'failed' }"
845
+ @click="filter('failed')">{{ spectreReport.failures }} failures</li>
846
+
847
+ <li
848
+ class="spectre-button spectre-summary-errors"
849
+ :class="{ disabled: spectreReport.errors == 0, inactive: statusFilter != null && statusFilter != 'error' }"
850
+ @click="filter('error')">{{ spectreReport.errors }} errors</li>
851
+
852
+ <li
853
+ class="spectre-button spectre-summary-total"
854
+ :class="{ disabled: spectreReport.total == 0, inactive: statusFilter != null }"
855
+ @click="showAll()">{{ spectreReport.total }} total</li>
856
+ </ul>
857
+ </section>
858
+
859
+ <section>
860
+ <ul class="spectre-section spectre-controls">
861
+ <li class="spectre-link" @click="collapseAll()">collapse all</li>
862
+ <li class="spectre-link" @click="expandAll()">expand all</li>
863
+ <li class="spectre-link spectre-controls-clear" :class="{ active: tagFilter.length > 0 || statusFilter != null }" @click="clearFilter()">
864
+ <span class="spectre-filter-count">{{ filteredResults.length }}/{{ spectreReport.run_infos.length }}<span>
865
+ </li>
866
+ </ul>
867
+ </section>
868
+
869
+ <section>
870
+ <ul class="spectre-result-subjects">
871
+ <li class="spectre-subject" v-for="(contexts, subject) in mappedResults">
872
+ <span class="spectre-subject-desc">{{ subject }}</span>
873
+
874
+ <ul class="spectre-result-contexts">
875
+ <li class="spectre-context" v-for="(runInfos, context) in contexts" :class="{ active: expandedContexts.includes(subject + '_' + context) }">
876
+ <span class="spectre-expander" @click="toggleContext(subject, context)"></span>
877
+ <span class="spectre-context-desc">{{ context }}</span>
878
+
879
+ <ul class="spectre-result-runinfos">
880
+ <li class="spectre-runinfo" v-for="runInfo in runInfos" :class="['spectre-status-' + runInfo.status, { active: shownDetails.includes(runInfo) }]">
881
+ <span class="spectre-expander" @click="showDetails(runInfo)"></span>
882
+ <span class="spectre-runinfo-description">
883
+ <span class="spectre-description-status"></span>
884
+ <span class="spectre-description-name">{{ runInfo.name }}</span>
885
+ <span class="spectre-description-data" v-if="runInfo.data">{{ runInfo.data }}</span>
886
+ <span class="spectre-description-subject">{{ subject }}</span>
887
+ <span class="spectre-description-spec">{{ runInfo.desc }}</span>
888
+ </span>
889
+
890
+ <div class="spectre-runinfo-details">
891
+ <fieldset>
892
+ <legend>Run Info</legend>
893
+
894
+ <table>
895
+ <tr><th>Status</th><td><span class="spectre-badge spectre-details-status">{{ runInfo.status }}</span></td></tr>
896
+ <tr><th>Name</th><td>{{ runInfo.name }}</td></tr>
897
+ <tr><th>Description</th><td>{{ runInfo.desc }}</td></tr>
898
+ <tr><th>Tags</th><td>
899
+ <ul class="spectre-tags">
900
+ <li class="spectre-tag" v-for="tag in runInfo.tags" @click="toggleTagFilter(tag)" :class="{ active: tagFilter.includes(tag)}">{{ tag }}</li>
901
+ </ul>
902
+ </td></tr>
903
+ <tr><th>File</th><td><span class="spectre-file">{{ runInfo.file }}<span></td></tr>
904
+ <tr><th>Started</th><td><span class="spectre-date">{{ runInfo.started }}<span></td></tr>
905
+ <tr><th>Finished</th><td><span class="spectre-date">{{ runInfo.finished }}<span></td></tr>
906
+ <tr><th>Duration</th><td>{{ runInfo.duration.toDurationString() }}</td></tr>
907
+ </table>
908
+ </fieldset>
909
+
910
+ <fieldset class="spectre-runinfo-data" v-if="runInfo.data">
911
+ <legend>Data</legend>
912
+ <pre>{{ runInfo.data }}</pre>
913
+ </fieldset>
914
+
915
+ <fieldset class="spectre-runinfo-properties" v-if="Object.keys(runInfo.properties).length > 0">
916
+ <legend>Properties</legend>
917
+
918
+ <table>
919
+ <tr v-for="(item, key) in runInfo.properties" :key="key"><th>{{ key }}</th><td>{{ item }}</td></tr>
920
+ </table>
921
+ </fieldset>
922
+
923
+ <fieldset class="spectre-runinfo-failure" v-if="runInfo.failure">
924
+ <legend>Failure</legend>
925
+
926
+ <table>
927
+ <tr><th>Message</th><td>{{ runInfo.failure.message }}</td></tr>
928
+ <tr><th>Expected</th><td>{{ runInfo.failure.expected }}</td></tr>
929
+ <tr><th>Actual</th><td>{{ runInfo.failure.actual }}</td></tr>
930
+ </table>
931
+ </fieldset>
932
+
933
+ <fieldset class="spectre-runinfo-error" v-if="runInfo.error">
934
+ <legend>Error</legend>
935
+
936
+ <table>
937
+ <tr><th>File</th><td><span class="spectre-file">{{ runInfo.error.file }}</span></td></tr>
938
+ <tr><th>Line</th><td>{{ runInfo.error.line }}</td></tr>
939
+ <tr><th>Type</th><td><span class="spectre-code">{{ runInfo.error.type }}</span></td></tr>
940
+ <tr><th>Message</th><td>{{ runInfo.error.message }}</td></tr>
941
+ </table>
942
+ </fieldset>
943
+
944
+ <fieldset class="spectre-runinfo-stacktrace" v-if="runInfo.error && runInfo.error.stack_strace">
945
+ <ul>
946
+ <li v-for="stackTraceEntry in runInfo.error.stack_strace">{{ stackTraceEntry }}</li>
947
+ </ul>
948
+ </fieldset>
949
+
950
+ <fieldset class="spectre-runinfo-log" v-if="runInfo.log && runInfo.log.length > 0">
951
+ <legend class="spectre-toggle" @click="toggleLog(runInfo)">Log</legend>
952
+
953
+ <ul class="spectre-log" v-if="shownLogs.includes(runInfo)">
954
+ <li v-for="logEntry in runInfo.log" class="spectre-log-entry">
955
+ <span class="spectre-log-timestamp">{{ logEntry[0] }}</span> <span class="spectre-log-level" :class="'spectre-log-level-' + logEntry[2]">{{ logEntry[2] }}</span> -- <span class="spectre-log-name">{{ logEntry[3] }}</span>: <span class="spectre-log-message">{{ logEntry[1] }}</span>
956
+ </li>
957
+ </ul>
958
+ </fieldset>
959
+ <div>
960
+ </li>
961
+ </ul>
962
+ </li>
963
+ </ul>
964
+ </li>
965
+ </ul>
966
+ </section>
967
+
968
+ <section id="spectre-environment">
969
+ <pre>#{ CGI::escapeHTML(YAML.dump json_report[:config]) }</pre>
970
+ </section>
971
+ </div>
972
+ </div>
973
+ </div>
974
+
975
+ <script>
976
+ Array.prototype.groupBy = function(callbackFn) {
977
+ const map = new Object();
978
+ this.forEach((item) => {
979
+ const key = callbackFn(item);
980
+ const collection = map[key];
981
+ if (!collection) {
982
+ map[key] = [item];
983
+ } else {
984
+ collection.push(item);
985
+ }
986
+ });
987
+ return map;
988
+ }
989
+
990
+ Object.prototype.map = function(callbackFn) {
991
+ return Object
992
+ .entries(this)
993
+ .map(callbackFn);
994
+ };
995
+
996
+ Array.prototype.toDict = function() {
997
+ return Object.assign({}, ...this.map((x) => ({[x[0]]: x[1]})));
998
+ };
999
+
1000
+ Array.prototype.distinct = function() {
1001
+ return this.filter((value, index, self) => self.indexOf(value) === index);
1002
+ };
1003
+
1004
+ Number.prototype.toDurationString = function() {
1005
+ let date = new Date(this * 1000);
1006
+ let hours = date.getUTCHours();
1007
+ let minutes = date.getUTCMinutes();
1008
+ let seconds = date.getUTCSeconds();
1009
+ let milliseconds = date.getUTCMilliseconds();
1010
+
1011
+ let durationString = '';
1012
+
1013
+ if (hours > 0) {
1014
+ durationString += `${hours}h`
1015
+ }
1016
+
1017
+ if (minutes > 0 || hours > 0) {
1018
+ if (durationString.length > 0) {
1019
+ durationString += ' ';
1020
+ }
1021
+
1022
+ durationString += `${minutes}m`
1023
+ }
1024
+
1025
+ if (seconds > 0 || minutes > 0) {
1026
+ if (durationString.length > 0) {
1027
+ durationString += ' ';
1028
+ }
1029
+
1030
+ durationString += `${seconds}s`
1031
+ }
1032
+
1033
+ if (milliseconds > 0) {
1034
+ if (durationString.length > 0) {
1035
+ durationString += ' ';
1036
+ }
1037
+
1038
+ durationString += `${milliseconds}ms`
1039
+ }
1040
+
1041
+ if (durationString.length == 0) {
1042
+ return `${this}s`
1043
+ }
1044
+
1045
+ return durationString;
1046
+ }
1047
+
1048
+ const { createApp } = Vue;
1049
+
1050
+ window.App = createApp({
1051
+ data() {
1052
+ return {
1053
+ spectreReport: #{json_report.to_json},
1054
+ statusFilter: null,
1055
+ tagFilter: [],
1056
+ shownDetails: [],
1057
+ shownLogs: [],
1058
+ expandedContexts: [],
1059
+ }
1060
+ },
1061
+ mounted() {
1062
+ this.expandAll();
1063
+ },
1064
+ computed: {
1065
+ filteredResults() {
1066
+ return this.spectreReport.run_infos
1067
+ .filter(x => this.statusFilter == null || x.status == this.statusFilter)
1068
+ .filter(x => this.tagFilter.length == 0 || x.tags.filter(x => this.tagFilter.includes(x)).length > 0);
1069
+ },
1070
+ mappedResults() {
1071
+ return this.filteredResults
1072
+ .groupBy(x => x.subject)
1073
+ .map(([key, val]) => [key, val.groupBy(x => x.context || '[main]')])
1074
+ .toDict();
1075
+ },
1076
+ spectreCommand() {
1077
+ let cmd = this.spectreReport.command;
1078
+ let filteredSpecs = this.filteredResults;
1079
+
1080
+ if (this.statusFilter == null && this.tagFilter.length > 0) {
1081
+ cmd += ` -t ${this.tagFilter.join(',')}`
1082
+
1083
+ } else if (this.statusFilter != null && filteredSpecs.length > 0) {
1084
+ cmd += ` -s ${this.filteredResults.map(x => x.name).join(',')}`
1085
+ }
1086
+
1087
+ return cmd;
1088
+ },
1089
+ },
1090
+ methods: {
1091
+ filter(status) {
1092
+ if (this.statusFilter == status) {
1093
+ this.statusFilter = null;
1094
+ return;
1095
+ }
1096
+
1097
+ if (this.spectreReport.run_infos.filter(x => x.status == status).length == 0) {
1098
+ return;
1099
+ }
1100
+
1101
+ this.statusFilter = status;
1102
+ },
1103
+ showAll() {
1104
+ this.statusFilter = null;
1105
+ },
1106
+ toggleTagFilter(tag) {
1107
+ let index = this.tagFilter.indexOf(tag);
1108
+
1109
+ if (index > -1) {
1110
+ this.tagFilter.splice(index, 1);
1111
+ } else {
1112
+ this.tagFilter.push(tag)
1113
+ }
1114
+ },
1115
+ clearFilter() {
1116
+ this.statusFilter = null;
1117
+ this.tagFilter = [];
1118
+ },
1119
+ showDetails(runInfo) {
1120
+ let index = this.shownDetails.indexOf(runInfo);
1121
+
1122
+ if (index > -1) {
1123
+ this.shownDetails.splice(index, 1);
1124
+ } else {
1125
+ this.shownDetails.push(runInfo)
1126
+ }
1127
+ },
1128
+ toggleContext(subject, context) {
1129
+ let key = subject + '_' + context;
1130
+
1131
+ let index = this.expandedContexts.indexOf(key);
1132
+
1133
+ if (index > -1) {
1134
+ this.expandedContexts.splice(index, 1);
1135
+ } else {
1136
+ this.expandedContexts.push(key)
1137
+ }
1138
+ },
1139
+ toggleLog(runInfo) {
1140
+ let index = this.shownLogs.indexOf(runInfo);
1141
+
1142
+ if (index > -1) {
1143
+ this.shownLogs.splice(index, 1);
1144
+ } else {
1145
+ this.shownLogs.push(runInfo)
1146
+ }
1147
+ },
1148
+ collapseAll() {
1149
+ this.expandedContexts = [];
1150
+ },
1151
+ expandAll() {
1152
+ this.expandedContexts = this.spectreReport.run_infos
1153
+ .map(x => x.subject + '_' + (x.context || '[main]'))
1154
+ .distinct();
1155
+ }
1156
+ }
1157
+ }).mount('#app')
1158
+ </script>
1159
+ </body>
1160
+ </html>
1161
+ HTML
1162
+
1163
+ Dir.mkdir @config['out_path'] unless Dir.exist? @config['out_path']
1164
+
1165
+ file_path = File.join(@config['out_path'], "spectre-html_#{now.strftime('%s')}.html")
1166
+
1167
+ File.write(file_path, html_str)
1168
+ end
1169
+ end
1170
+ end
1171
+ end