spectre-core 1.12.2 → 1.13.0

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