@freshworks/shiftleft-tools 1.1.8

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.
Files changed (106) hide show
  1. package/README.md +351 -0
  2. package/bin/shiftleft.js +95 -0
  3. package/package.json +57 -0
  4. package/src/commands/doctor.js +208 -0
  5. package/src/commands/init-postman.js +298 -0
  6. package/src/commands/init-rules.js +78 -0
  7. package/src/commands/link.js +172 -0
  8. package/src/commands/protect.js +61 -0
  9. package/src/commands/run-tests.js +182 -0
  10. package/src/commands/setup-pipeline.js +209 -0
  11. package/src/commands/update.js +203 -0
  12. package/src/index.js +4 -0
  13. package/src/utils/copy-tree.js +98 -0
  14. package/src/utils/gitignore.js +26 -0
  15. package/src/utils/logger.js +9 -0
  16. package/src/utils/manifest.js +145 -0
  17. package/src/utils/stack.js +80 -0
  18. package/src/utils/template.js +135 -0
  19. package/templates/AGENTS.md +109 -0
  20. package/templates/CLAUDE.md +3 -0
  21. package/templates/jenkins/Jenkinsfile-java.groovy +432 -0
  22. package/templates/jenkins/Jenkinsfile-node.groovy +450 -0
  23. package/templates/postman/.husky/pre-commit +19 -0
  24. package/templates/postman/.prettierrc.json +5 -0
  25. package/templates/postman/README.md.ejs +147 -0
  26. package/templates/postman/collections/01-core.json.ejs +91 -0
  27. package/templates/postman/config/local.json.ejs +12 -0
  28. package/templates/postman/config/staging.json.ejs +26 -0
  29. package/templates/postman/environments/local.postman_environment.json.ejs +31 -0
  30. package/templates/postman/environments/staging.postman_environment.json.ejs +31 -0
  31. package/templates/postman/gitignore +16 -0
  32. package/templates/postman/npmrc +31 -0
  33. package/templates/postman/package.json.ejs +66 -0
  34. package/templates/postman/run-all-shim.sh +16 -0
  35. package/templates/postman/scripts/auth/generate-jwt.sh +113 -0
  36. package/templates/postman/scripts/auth/get-issuer-secret.sh +140 -0
  37. package/templates/postman/scripts/infra/start-mocks.sh +138 -0
  38. package/templates/postman/scripts/infra/stop-mocks.sh +43 -0
  39. package/templates/postman/scripts/lib/api_coverage.py +1122 -0
  40. package/templates/postman/scripts/lib/cleanup-reports.sh +101 -0
  41. package/templates/postman/scripts/lib/cleanup-stryker.sh +44 -0
  42. package/templates/postman/scripts/lib/report_combined.py +527 -0
  43. package/templates/postman/scripts/lib/report_consolidated.py +363 -0
  44. package/templates/postman/scripts/lib/report_generator.py +121 -0
  45. package/templates/postman/scripts/lib/report_migration.py +156 -0
  46. package/templates/postman/scripts/lib/report_mutation.py +110 -0
  47. package/templates/postman/scripts/lib/report_unit.py +353 -0
  48. package/templates/postman/scripts/lib/report_utils.py +973 -0
  49. package/templates/postman/scripts/report-generators/generate-consolidated-report.sh +445 -0
  50. package/templates/postman/scripts/report-generators/java-api-coverage-matrix.sh +257 -0
  51. package/templates/postman/scripts/report-generators/mutation-report.sh +672 -0
  52. package/templates/postman/scripts/report-generators/node-api-coverage-matrix.sh +167 -0
  53. package/templates/postman/scripts/report-generators/stage-report-artifacts.sh +27 -0
  54. package/templates/postman/scripts/run-all.sh +452 -0
  55. package/templates/postman/scripts/runners/run-mutation-tests.sh +113 -0
  56. package/templates/postman/scripts/runners/run-tests-local.sh +936 -0
  57. package/templates/postman/scripts/runners/run-tests-staging.sh +741 -0
  58. package/templates/postman-node/README.md.ejs +26 -0
  59. package/templates/postman-node/collections/crud/01-bootstrap.json.ejs +34 -0
  60. package/templates/postman-node/config/local.json.ejs +46 -0
  61. package/templates/postman-node/config/staging.json.ejs +31 -0
  62. package/templates/postman-node/local.test.env.ejs +3 -0
  63. package/templates/postman-node/mocks/external.js +14 -0
  64. package/templates/postman-node/package.json.ejs +39 -0
  65. package/templates/postman-node/requirements.txt +1 -0
  66. package/templates/postman-node/scripts/database/cleanup-mysql.sh +12 -0
  67. package/templates/postman-node/scripts/database/run-migrations.js +29 -0
  68. package/templates/postman-node/scripts/database/start-mysql.sh +34 -0
  69. package/templates/postman-node/scripts/database/wait-for-mysql.sh +36 -0
  70. package/templates/postman-node/scripts/lib/api_coverage_node.py +1137 -0
  71. package/templates/postman-node/scripts/lib/fetch-jwt.sh +86 -0
  72. package/templates/postman-node/scripts/lib/run-newman.sh +104 -0
  73. package/templates/postman-node/scripts/lib/setup-database.sh +55 -0
  74. package/templates/postman-node/scripts/lib/start-app.sh +48 -0
  75. package/templates/postman-node/scripts/lib/utils.sh +114 -0
  76. package/templates/postman-node/scripts/report-generators/stage-report-artifacts.sh +26 -0
  77. package/templates/postman-node/scripts/run-all.sh +303 -0
  78. package/templates/postman-node/scripts/runners/run-tests.sh +123 -0
  79. package/templates/postman-node/scripts/setup-mocks.js.ejs +29 -0
  80. package/templates/postman-node/stryker.config.js.ejs +51 -0
  81. package/templates/rules/local-test-setup.mdc +420 -0
  82. package/templates/rules/testing-node.mdc +66 -0
  83. package/templates/rules/testing.mdc +248 -0
  84. package/templates/skills/_shared/postman-standards.md +380 -0
  85. package/templates/skills/enhance-test-pipeline/SKILL-java.md +483 -0
  86. package/templates/skills/enhance-test-pipeline/SKILL-node.md +431 -0
  87. package/templates/skills/enhance-test-pipeline/SKILL.md +9 -0
  88. package/templates/skills/review-test-suite/SKILL-java.md +137 -0
  89. package/templates/skills/review-test-suite/SKILL-node.md +78 -0
  90. package/templates/skills/review-test-suite/SKILL.md +9 -0
  91. package/templates/skills/run-test-suite/SKILL-java.md +186 -0
  92. package/templates/skills/run-test-suite/SKILL-node.md +191 -0
  93. package/templates/skills/run-test-suite/SKILL.md +9 -0
  94. package/templates/skills/setup-api-tests/SKILL-java.md +1094 -0
  95. package/templates/skills/setup-api-tests/SKILL-node.md +141 -0
  96. package/templates/skills/setup-api-tests/SKILL.md +9 -0
  97. package/templates/skills/setup-mutation-tests/SKILL-java.md +303 -0
  98. package/templates/skills/setup-mutation-tests/SKILL-node.md +408 -0
  99. package/templates/skills/setup-mutation-tests/SKILL.md +9 -0
  100. package/templates/skills/setup-test-pipeline/SKILL-java.md +454 -0
  101. package/templates/skills/setup-test-pipeline/SKILL-node.md +318 -0
  102. package/templates/skills/setup-test-pipeline/SKILL.md +9 -0
  103. package/templates/skills/write-api-tests/SKILL-java.md +115 -0
  104. package/templates/skills/write-api-tests/SKILL-node.md +83 -0
  105. package/templates/skills/write-api-tests/SKILL.md +9 -0
  106. package/templates/stryker.config.js +50 -0
@@ -0,0 +1,973 @@
1
+ """
2
+ Shared utilities, CSS, and UI component builders for all report types.
3
+ """
4
+
5
+ import glob
6
+ import os
7
+ import json
8
+ from datetime import datetime
9
+ from html import escape as esc
10
+
11
+
12
+ # =============================================================================
13
+ # Jenkins-safe report helpers (CSP-compatible tabs, relative links)
14
+ # =============================================================================
15
+
16
+ def relative_href(from_html_path, to_path):
17
+ """Return a relative URL between HTML files (works in Jenkins HTML Publisher)."""
18
+ if not to_path or not from_html_path:
19
+ return None
20
+ from_dir = os.path.dirname(os.path.abspath(from_html_path))
21
+ if os.path.isdir(to_path):
22
+ to_path = os.path.join(to_path, 'index.html')
23
+ to_abs = os.path.abspath(to_path)
24
+ if not os.path.isfile(to_abs):
25
+ return None
26
+ rel = os.path.relpath(to_abs, from_dir)
27
+ return rel.replace(os.sep, '/')
28
+
29
+
30
+ def resolve_staged_artifact_href(output_html, relative_path):
31
+ """Use artifacts copied under postman/reports/artifacts/ for Jenkins."""
32
+ if not output_html:
33
+ return None
34
+ output_dir = os.path.dirname(os.path.abspath(output_html))
35
+ staged = os.path.join(output_dir, relative_path)
36
+ if os.path.isfile(staged):
37
+ return relative_path.replace(os.sep, '/')
38
+ return None
39
+
40
+
41
+ def find_pit_index_path(mutations_xml):
42
+ if not mutations_xml or not os.path.isfile(mutations_xml):
43
+ return None
44
+ pit_dir = os.path.dirname(os.path.abspath(mutations_xml))
45
+ index_path = os.path.join(pit_dir, 'index.html')
46
+ if os.path.isfile(index_path):
47
+ return index_path
48
+ matches = glob.glob(os.path.join(pit_dir, '**/index.html'), recursive=True)
49
+ return matches[0] if matches else None
50
+
51
+
52
+ def resolve_jacoco_href(output_html, jacoco_xml):
53
+ staged = resolve_staged_artifact_href(output_html, 'artifacts/jacoco/index.html')
54
+ if staged:
55
+ return staged
56
+ if jacoco_xml:
57
+ index_path = os.path.join(os.path.dirname(os.path.abspath(jacoco_xml)), 'index.html')
58
+ return relative_href(output_html, index_path)
59
+ return None
60
+
61
+
62
+ def resolve_pit_href(output_html, mutations_xml):
63
+ staged = resolve_staged_artifact_href(output_html, 'artifacts/pit/index.html')
64
+ if staged:
65
+ return staged
66
+ pit_index = find_pit_index_path(mutations_xml)
67
+ return relative_href(output_html, pit_index) if pit_index else None
68
+
69
+
70
+ def resolve_sibling_report_href(output_html, report_path):
71
+ if not report_path:
72
+ return None
73
+ if not os.path.isabs(report_path):
74
+ return report_path.replace(os.sep, '/')
75
+ return relative_href(output_html, report_path)
76
+
77
+
78
+ def report_detail_links_row(cards):
79
+ """Render a row of links to detailed reports (no inline JS)."""
80
+ visible = [(subtitle, title, href) for subtitle, title, href in cards if href]
81
+ if not visible:
82
+ return ''
83
+ html = (
84
+ '<div style="margin-bottom: 1.5rem;">'
85
+ '<div style="background: var(--color-card); border: 1px solid var(--color-border); '
86
+ 'border-radius: var(--radius-sm); padding: 1.5rem; box-shadow: var(--shadow-sm);">'
87
+ '<div style="display: flex; justify-content: space-between; align-items: center; '
88
+ 'gap: 2rem; flex-wrap: wrap;">'
89
+ )
90
+ for subtitle, title, href in visible:
91
+ html += f'''
92
+ <div style="flex: 1; min-width: 250px;">
93
+ <div style="color: var(--color-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem;">{subtitle}</div>
94
+ <a href="{href}" class="report-detail-link">
95
+ <span>{title}</span>
96
+ <span class="report-detail-link-arrow">→</span>
97
+ </a>
98
+ </div>'''
99
+ html += '</div></div></div>'
100
+ return html
101
+
102
+
103
+ def build_css_tabs(tab_prefix, tabs, default_index=0):
104
+ """
105
+ Build CSP-safe tabs using radio inputs (no JavaScript).
106
+ tabs: list of (suffix, label, content_html)
107
+ """
108
+ group_name = f"{tab_prefix}-group"
109
+ style_rules = []
110
+ parts = ['<div class="css-tabs">']
111
+ for i, (suffix, _label, _content) in enumerate(tabs):
112
+ radio_id = f"{tab_prefix}-{suffix}"
113
+ panel_id = f"{tab_prefix}-panel-{suffix}"
114
+ checked = ' checked' if i == default_index else ''
115
+ parts.append(f'<input type="radio" name="{group_name}" id="{radio_id}"{checked}>')
116
+ style_rules.append(f'#{radio_id}:checked ~ #{panel_id} {{ display: block; }}')
117
+ style_rules.append(
118
+ f'#{radio_id}:checked ~ .tab-labels label[for="{radio_id}"] {{ '
119
+ f'color: var(--color-primary); border-bottom-color: var(--color-primary); }}'
120
+ )
121
+ parts.append('<div class="tab-labels">')
122
+ for suffix, label, _content in tabs:
123
+ radio_id = f"{tab_prefix}-{suffix}"
124
+ parts.append(f'<label for="{radio_id}" class="tab">{label}</label>')
125
+ parts.append('</div>')
126
+ for suffix, _label, panel_content in tabs:
127
+ panel_id = f"{tab_prefix}-panel-{suffix}"
128
+ parts.append(f'<div class="tab-panel" id="{panel_id}">{panel_content}</div>')
129
+ parts.append('</div>')
130
+ style_block = '<style>\n' + '\n'.join(style_rules) + '\n</style>\n'
131
+ return style_block + ''.join(parts)
132
+
133
+
134
+ # =============================================================================
135
+ # Shared CSS - Single source of truth for all reports
136
+ # =============================================================================
137
+
138
+ SHARED_CSS = '''
139
+ :root {
140
+ --color-primary: #6366f1;
141
+ --color-primary-light: #818cf8;
142
+ --color-success: #22c55e;
143
+ --color-success-bg: #f0fdf4;
144
+ --color-warning: #f59e0b;
145
+ --color-warning-bg: #fffbeb;
146
+ --color-error: #ef4444;
147
+ --color-error-bg: #fef2f2;
148
+ --color-info: #3b82f6;
149
+ --color-muted: #64748b;
150
+ --color-bg: #f8fafc;
151
+ --color-card: #ffffff;
152
+ --color-border: #e2e8f0;
153
+ --color-text: #334155;
154
+ --color-heading: #0f172a;
155
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
156
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
157
+ --shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
158
+ --radius: 16px;
159
+ --radius-sm: 10px;
160
+ }
161
+
162
+ * { margin: 0; padding: 0; box-sizing: border-box; }
163
+
164
+ body {
165
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
166
+ background: var(--color-bg);
167
+ color: var(--color-text);
168
+ line-height: 1.6;
169
+ padding: 2rem 1.5rem;
170
+ }
171
+
172
+ .container { max-width: 1600px; margin: 0 auto; padding: 0 1rem; }
173
+
174
+ /* Header */
175
+ .header {
176
+ display: flex;
177
+ justify-content: space-between;
178
+ align-items: flex-start;
179
+ margin-bottom: 2.5rem;
180
+ padding-bottom: 1.5rem;
181
+ border-bottom: 1px solid var(--color-border);
182
+ }
183
+
184
+ .header-left .logo {
185
+ font-size: 0.75rem;
186
+ font-weight: 600;
187
+ color: var(--color-primary);
188
+ text-transform: uppercase;
189
+ letter-spacing: 0.1em;
190
+ margin-bottom: 0.5rem;
191
+ }
192
+
193
+ .header h1 {
194
+ font-size: 1.875rem;
195
+ font-weight: 700;
196
+ color: var(--color-heading);
197
+ letter-spacing: -0.02em;
198
+ }
199
+
200
+ .header .timestamp {
201
+ color: var(--color-muted);
202
+ font-size: 0.875rem;
203
+ margin-top: 0.25rem;
204
+ }
205
+
206
+ .health-badge {
207
+ display: inline-flex;
208
+ align-items: center;
209
+ gap: 0.5rem;
210
+ padding: 0.625rem 1.25rem;
211
+ border-radius: 50px;
212
+ font-weight: 600;
213
+ font-size: 0.9rem;
214
+ box-shadow: var(--shadow-sm);
215
+ }
216
+
217
+ .health-badge.excellent { background: linear-gradient(135deg, #d1fae5, #a7f3d0); color: #047857; }
218
+ .health-badge.good { background: linear-gradient(135deg, #fef3c7, #fde68a); color: #b45309; }
219
+ .health-badge.fair { background: linear-gradient(135deg, #fed7aa, #fdba74); color: #c2410c; }
220
+ .health-badge.poor { background: linear-gradient(135deg, #fee2e2, #fecaca); color: #b91c1c; }
221
+
222
+ /* Sections */
223
+ .section { margin-bottom: 2.5rem; }
224
+
225
+ .section-title {
226
+ font-size: 1.125rem;
227
+ font-weight: 600;
228
+ color: var(--color-heading);
229
+ margin-bottom: 1.25rem;
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 0.75rem;
233
+ }
234
+
235
+ .section-title .count {
236
+ background: var(--color-border);
237
+ padding: 0.25rem 0.75rem;
238
+ border-radius: 20px;
239
+ font-size: 0.75rem;
240
+ font-weight: 500;
241
+ color: var(--color-muted);
242
+ }
243
+
244
+ /* Summary Grid */
245
+ .summary-grid {
246
+ display: grid;
247
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
248
+ gap: 1.25rem;
249
+ margin-bottom: 2.5rem;
250
+ }
251
+
252
+ .summary-card {
253
+ background: var(--color-card);
254
+ border: 1px solid var(--color-border);
255
+ border-radius: var(--radius);
256
+ padding: 1.5rem 1.25rem;
257
+ text-align: center;
258
+ box-shadow: var(--shadow-sm);
259
+ transition: transform 0.2s, box-shadow 0.2s;
260
+ }
261
+
262
+ .summary-card:hover {
263
+ transform: translateY(-2px);
264
+ box-shadow: var(--shadow-md);
265
+ }
266
+
267
+ .summary-card.health { border-width: 2px; }
268
+ .summary-card.health.excellent { border-color: var(--color-success); background: var(--color-success-bg); }
269
+ .summary-card.health.good { border-color: var(--color-warning); background: var(--color-warning-bg); }
270
+ .summary-card.health.poor { border-color: var(--color-error); background: var(--color-error-bg); }
271
+
272
+ .summary-card .icon { font-size: 1.75rem; margin-bottom: 0.375rem; }
273
+ .summary-card .value {
274
+ font-size: 2.25rem;
275
+ font-weight: 700;
276
+ color: var(--color-primary);
277
+ line-height: 1.2;
278
+ }
279
+ .summary-card .label {
280
+ color: var(--color-muted);
281
+ font-size: 0.7rem;
282
+ font-weight: 500;
283
+ text-transform: uppercase;
284
+ letter-spacing: 0.08em;
285
+ margin-top: 0.375rem;
286
+ }
287
+ .summary-card .detail {
288
+ color: var(--color-muted);
289
+ font-size: 0.75rem;
290
+ margin-top: 0.25rem;
291
+ }
292
+ .summary-card.success .value { color: var(--color-success); }
293
+ .summary-card.warning .value { color: var(--color-warning); }
294
+ .summary-card.error .value { color: var(--color-error); }
295
+
296
+ /* Progress Bar */
297
+ .progress-container {
298
+ background: var(--color-card);
299
+ border: 1px solid var(--color-border);
300
+ border-radius: var(--radius);
301
+ padding: 1.75rem;
302
+ margin-bottom: 2.5rem;
303
+ box-shadow: var(--shadow-sm);
304
+ }
305
+
306
+ .progress-header {
307
+ display: flex;
308
+ justify-content: space-between;
309
+ align-items: center;
310
+ margin-bottom: 1.25rem;
311
+ }
312
+
313
+ .progress-title { font-weight: 600; font-size: 1.1rem; color: var(--color-heading); }
314
+ .progress-value { font-size: 2.25rem; font-weight: 700; }
315
+
316
+ .progress-bar {
317
+ height: 14px;
318
+ background: var(--color-border);
319
+ border-radius: 7px;
320
+ overflow: hidden;
321
+ }
322
+
323
+ .progress-bar.small { height: 8px; border-radius: 4px; }
324
+
325
+ .progress-fill {
326
+ height: 100%;
327
+ border-radius: 7px;
328
+ transition: width 0.5s ease-out;
329
+ }
330
+
331
+ .progress-fill.success { background: linear-gradient(90deg, #22c55e, #4ade80); }
332
+ .progress-fill.warning { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
333
+ .progress-fill.error { background: linear-gradient(90deg, #ef4444, #f87171); }
334
+
335
+ /* Tables */
336
+ table {
337
+ width: 100%;
338
+ border-collapse: collapse;
339
+ background: var(--color-card);
340
+ border: 1px solid var(--color-border);
341
+ border-radius: var(--radius-sm);
342
+ overflow: hidden;
343
+ font-size: 0.875rem;
344
+ box-shadow: var(--shadow-sm);
345
+ }
346
+
347
+ th, td {
348
+ padding: 0.875rem 1.125rem;
349
+ text-align: left;
350
+ border-bottom: 1px solid var(--color-border);
351
+ }
352
+
353
+ th {
354
+ background: #f1f5f9;
355
+ font-weight: 600;
356
+ font-size: 0.7rem;
357
+ text-transform: uppercase;
358
+ letter-spacing: 0.06em;
359
+ color: var(--color-muted);
360
+ }
361
+
362
+ td.center { text-align: center; }
363
+ tr:hover { background: #fafbfc; }
364
+ tr:last-child td { border-bottom: none; }
365
+
366
+ /* Badges */
367
+ .badge {
368
+ display: inline-block;
369
+ padding: 0.3rem 0.875rem;
370
+ border-radius: 20px;
371
+ font-size: 0.7rem;
372
+ font-weight: 600;
373
+ }
374
+
375
+ .badge-success { background: linear-gradient(135deg, #d1fae5, #a7f3d0); color: #047857; }
376
+ .badge-warning { background: linear-gradient(135deg, #fef3c7, #fde68a); color: #b45309; }
377
+ .badge-error { background: linear-gradient(135deg, #fee2e2, #fecaca); color: #b91c1c; }
378
+ .badge-info { background: linear-gradient(135deg, #e0e7ff, #c7d2fe); color: #3730a3; }
379
+
380
+ /* Cards */
381
+ .card {
382
+ background: var(--color-card);
383
+ border: 1px solid var(--color-border);
384
+ border-radius: var(--radius-sm);
385
+ padding: 1.25rem;
386
+ box-shadow: var(--shadow-sm);
387
+ transition: transform 0.2s, box-shadow 0.2s;
388
+ }
389
+
390
+ .card:hover {
391
+ transform: translateY(-2px);
392
+ box-shadow: var(--shadow-md);
393
+ }
394
+
395
+ /* Grid layouts */
396
+ .grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.25rem; }
397
+ .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
398
+ .grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.25rem; }
399
+
400
+ /* Code styling */
401
+ .code {
402
+ font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
403
+ font-size: 0.875rem;
404
+ color: var(--color-heading);
405
+ }
406
+
407
+ /* Tabs */
408
+ .tabs {
409
+ display: flex;
410
+ gap: 0.5rem;
411
+ border-bottom: 2px solid var(--color-border);
412
+ margin-bottom: 1.5rem;
413
+ }
414
+
415
+ .tab {
416
+ padding: 0.75rem 1.5rem;
417
+ background: transparent;
418
+ border: none;
419
+ border-bottom: 3px solid transparent;
420
+ color: var(--color-muted);
421
+ font-weight: 500;
422
+ font-size: 0.9rem;
423
+ cursor: pointer;
424
+ transition: all 0.2s;
425
+ margin-bottom: -2px;
426
+ }
427
+
428
+ .tab:hover {
429
+ color: var(--color-heading);
430
+ background: var(--color-bg);
431
+ }
432
+
433
+ .tab.active {
434
+ color: var(--color-primary);
435
+ border-bottom-color: var(--color-primary);
436
+ }
437
+
438
+ .tab-content {
439
+ display: none;
440
+ }
441
+
442
+ .tab-content.active {
443
+ display: block;
444
+ }
445
+
446
+ .top-tab-content {
447
+ display: none;
448
+ }
449
+
450
+ .top-tab-content.active {
451
+ display: block;
452
+ }
453
+
454
+ .sub-tab-content {
455
+ display: none;
456
+ }
457
+
458
+ .sub-tab-content.active {
459
+ display: block;
460
+ }
461
+
462
+ /* CSS-only tabs (Jenkins Content-Security-Policy compatible) */
463
+ .css-tabs {
464
+ position: relative;
465
+ }
466
+
467
+ .css-tabs > input[type="radio"] {
468
+ position: absolute;
469
+ opacity: 0;
470
+ width: 0;
471
+ height: 0;
472
+ pointer-events: none;
473
+ }
474
+
475
+ .css-tabs > .tab-labels {
476
+ display: flex;
477
+ gap: 0.5rem;
478
+ border-bottom: 2px solid var(--color-border);
479
+ margin-bottom: 1.5rem;
480
+ }
481
+
482
+ .css-tabs > .tab-panel {
483
+ display: none;
484
+ }
485
+
486
+ .report-detail-link {
487
+ display: flex;
488
+ align-items: center;
489
+ justify-content: space-between;
490
+ padding: 0.875rem 1.125rem;
491
+ background: var(--color-bg);
492
+ border: 1px solid var(--color-border);
493
+ border-radius: var(--radius-sm);
494
+ color: var(--color-heading);
495
+ text-decoration: none;
496
+ font-weight: 500;
497
+ font-size: 0.9rem;
498
+ transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
499
+ box-shadow: var(--shadow-sm);
500
+ }
501
+
502
+ .report-detail-link:hover {
503
+ transform: translateY(-2px);
504
+ box-shadow: var(--shadow-md);
505
+ border-color: var(--color-primary);
506
+ }
507
+
508
+ .report-detail-link-arrow {
509
+ color: var(--color-primary);
510
+ }
511
+
512
+ #top-tabs {
513
+ border-bottom-width: 3px;
514
+ margin-bottom: 2rem;
515
+ }
516
+
517
+ #top-tabs .tab {
518
+ font-size: 1rem;
519
+ padding: 0.875rem 2rem;
520
+ font-weight: 600;
521
+ }
522
+
523
+ /* Feature sections */
524
+ .feature-grid {
525
+ display: grid;
526
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
527
+ gap: 1.25rem;
528
+ }
529
+
530
+ .feature-section {
531
+ background: var(--color-card);
532
+ border: 1px solid var(--color-border);
533
+ border-radius: var(--radius-sm);
534
+ padding: 1.25rem;
535
+ box-shadow: var(--shadow-sm);
536
+ }
537
+
538
+ .feature-section h4 {
539
+ font-size: 0.95rem;
540
+ margin-bottom: 0.875rem;
541
+ color: var(--color-heading);
542
+ }
543
+
544
+ .feature-list { list-style: none; max-height: 220px; overflow-y: auto; }
545
+ .feature-list li {
546
+ padding: 0.625rem 0;
547
+ border-bottom: 1px solid var(--color-border);
548
+ font-size: 0.825rem;
549
+ }
550
+ .feature-list li:last-child { border-bottom: none; }
551
+
552
+ /* Method badges for API coverage */
553
+ .method {
554
+ display: inline-block;
555
+ padding: 0.15rem 0.4rem;
556
+ border-radius: 4px;
557
+ font-weight: 600;
558
+ font-size: 0.6rem;
559
+ color: white;
560
+ }
561
+ .method.GET { background: #22c55e; }
562
+ .method.POST { background: #3b82f6; }
563
+ .method.PUT { background: #f59e0b; }
564
+ .method.DELETE { background: #ef4444; }
565
+ .method.PATCH { background: #8b5cf6; }
566
+
567
+ /* Footer */
568
+ footer {
569
+ margin-top: 3rem;
570
+ text-align: center;
571
+ color: var(--color-muted);
572
+ font-size: 0.85rem;
573
+ padding-top: 1.5rem;
574
+ border-top: 1px solid var(--color-border);
575
+ }
576
+
577
+ footer span {
578
+ opacity: 0.5;
579
+ margin: 0 0.5rem;
580
+ }
581
+
582
+ /* Responsive */
583
+ @media (max-width: 768px) {
584
+ body { padding: 1.25rem; }
585
+ .header { flex-direction: column; gap: 1rem; }
586
+ .summary-grid { grid-template-columns: repeat(2, 1fr); }
587
+ .feature-grid { grid-template-columns: 1fr; }
588
+ .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
589
+ }
590
+
591
+ /* ========== Coverage Report Specific Styles ========== */
592
+
593
+ /* Executive Summary - 8-column grid */
594
+ .executive-summary {
595
+ display: grid;
596
+ grid-template-columns: repeat(8, 1fr);
597
+ gap: 1.25rem;
598
+ margin-bottom: 2.5rem;
599
+ width: 100%;
600
+ }
601
+
602
+ .summary-card {
603
+ min-height: 110px;
604
+ display: flex;
605
+ flex-direction: column;
606
+ justify-content: center;
607
+ }
608
+
609
+ .summary-card.health { padding: 1.25rem 1rem; }
610
+ .summary-card.health.fair { border-color: var(--color-warning); background: var(--color-warning-bg); }
611
+
612
+ .summary-card .sub-value {
613
+ color: var(--color-primary);
614
+ font-size: 0.875rem;
615
+ font-weight: 500;
616
+ margin-top: 0.25rem;
617
+ }
618
+
619
+ .summary-card .health-label {
620
+ font-size: 1.1rem;
621
+ font-weight: 600;
622
+ color: var(--color-heading);
623
+ }
624
+
625
+ .summary-card .health-desc {
626
+ font-size: 0.75rem;
627
+ color: var(--color-muted);
628
+ margin-top: 0.375rem;
629
+ line-height: 1.4;
630
+ }
631
+
632
+ /* Coverage Grid */
633
+ .coverage-grid {
634
+ display: grid;
635
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
636
+ gap: 1.25rem;
637
+ }
638
+
639
+ .coverage-item {
640
+ background: var(--color-card);
641
+ border: 1px solid var(--color-border);
642
+ border-radius: var(--radius-sm);
643
+ padding: 1.25rem;
644
+ box-shadow: var(--shadow-sm);
645
+ transition: transform 0.2s, box-shadow 0.2s;
646
+ }
647
+
648
+ .coverage-item:hover {
649
+ transform: translateY(-2px);
650
+ box-shadow: var(--shadow-md);
651
+ }
652
+
653
+ .coverage-item .header {
654
+ display: flex;
655
+ justify-content: space-between;
656
+ align-items: center;
657
+ margin-bottom: 0.875rem;
658
+ padding-bottom: 0;
659
+ border-bottom: none;
660
+ }
661
+
662
+ .coverage-item .metric-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); }
663
+ .coverage-item .metric-value { font-weight: 700; font-size: 1.125rem; }
664
+ .coverage-item .details { margin-top: 0.625rem; font-size: 0.8rem; color: var(--color-muted); }
665
+
666
+ /* Gaps Section */
667
+ .gaps-section {
668
+ background: var(--color-error-bg);
669
+ border: 1px solid #fecaca;
670
+ border-radius: var(--radius-sm);
671
+ padding: 1.25rem;
672
+ margin-bottom: 1.25rem;
673
+ }
674
+
675
+ .gaps-section.warning { background: var(--color-warning-bg); border-color: #fde68a; }
676
+
677
+ .gaps-section .title {
678
+ font-weight: 600;
679
+ font-size: 0.95rem;
680
+ color: var(--color-error);
681
+ margin-bottom: 0.875rem;
682
+ display: flex;
683
+ align-items: center;
684
+ gap: 0.5rem;
685
+ }
686
+
687
+ .gaps-section.warning .title { color: var(--color-warning); }
688
+
689
+ .gap-list { display: flex; flex-wrap: wrap; gap: 0.5rem; }
690
+
691
+ .gap-item {
692
+ background: white;
693
+ padding: 0.375rem 0.625rem;
694
+ border-radius: 6px;
695
+ font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
696
+ font-size: 0.75rem;
697
+ border: 1px solid #fecaca;
698
+ transition: transform 0.15s;
699
+ }
700
+
701
+ .gap-item:hover { transform: scale(1.02); }
702
+ .gaps-section.warning .gap-item { border-color: #fde68a; }
703
+
704
+ /* Method badges - detailed colors */
705
+ .method {
706
+ font-weight: 600;
707
+ padding: 0.25rem 0.625rem;
708
+ border-radius: 6px;
709
+ font-size: 0.7rem;
710
+ display: inline-block;
711
+ min-width: 55px;
712
+ }
713
+
714
+ .method-GET { background: #dbeafe; color: #1d4ed8; }
715
+ .method-POST { background: #d1fae5; color: #047857; }
716
+ .method-PUT { background: #fef3c7; color: #b45309; }
717
+ .method-PATCH { background: #ede9fe; color: #6d28d9; }
718
+ .method-DELETE { background: #fee2e2; color: #b91c1c; }
719
+
720
+ .path {
721
+ font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
722
+ font-size: 0.825rem;
723
+ color: var(--color-heading);
724
+ }
725
+
726
+ .status-icon { font-size: 1rem; }
727
+
728
+ /* Collapsible Details */
729
+ details {
730
+ background: var(--color-card);
731
+ border: 1px solid var(--color-border);
732
+ border-radius: var(--radius-sm);
733
+ margin-bottom: 1rem;
734
+ box-shadow: var(--shadow-sm);
735
+ }
736
+
737
+ summary {
738
+ padding: 1rem 1.25rem;
739
+ cursor: pointer;
740
+ font-weight: 600;
741
+ display: flex;
742
+ justify-content: space-between;
743
+ align-items: center;
744
+ transition: background 0.15s;
745
+ }
746
+
747
+ summary:hover { background: #fafbfc; }
748
+ details[open] summary { border-bottom: 1px solid var(--color-border); }
749
+ details .content { padding: 1.25rem; }
750
+
751
+ /* Table enhancements for coverage */
752
+ th, td { padding: 0.75rem 0.625rem; text-align: center; vertical-align: middle; }
753
+ td.left { text-align: left; }
754
+
755
+ /* Extended Responsive */
756
+ @media (max-width: 1200px) {
757
+ .executive-summary { grid-template-columns: repeat(4, 1fr); }
758
+ }
759
+
760
+ @media (max-width: 992px) {
761
+ .executive-summary { grid-template-columns: repeat(2, 1fr); }
762
+ .coverage-grid { grid-template-columns: repeat(2, 1fr); }
763
+ }
764
+
765
+ @media (max-width: 640px) {
766
+ .executive-summary { grid-template-columns: 1fr; }
767
+ .coverage-grid { grid-template-columns: 1fr; }
768
+ }
769
+ '''
770
+
771
+
772
+ def get_css():
773
+ return SHARED_CSS
774
+
775
+
776
+ def get_html_template(title, logo, content, timestamp=None, health_badge=None, subtitle=None, footer_text="Generated by Report Generator"):
777
+ timestamp = timestamp or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
778
+
779
+ health_html = ""
780
+ if health_badge:
781
+ badge_class = esc(str(health_badge.get('class', 'good')))
782
+ badge_icon = esc(str(health_badge.get('icon', '')))
783
+ badge_label = esc(str(health_badge.get('label', '')))
784
+ health_html = f'''
785
+ <div class="health-badge {badge_class}">
786
+ {badge_icon} {badge_label}
787
+ </div>
788
+ '''
789
+
790
+ subtitle_html = f'<p class="timestamp">{esc(subtitle)}</p>' if subtitle else ""
791
+ safe_title = esc(title)
792
+ safe_logo = esc(logo)
793
+ safe_footer = esc(footer_text)
794
+ safe_timestamp = esc(timestamp)
795
+
796
+ return f'''<!DOCTYPE html>
797
+ <html lang="en">
798
+ <head>
799
+ <meta charset="UTF-8">
800
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
801
+ <title>{safe_title} | {safe_logo}</title>
802
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
803
+ <style>{SHARED_CSS}</style>
804
+ </head>
805
+ <body>
806
+ <div class="container">
807
+ <div class="header">
808
+ <div class="header-left">
809
+ <div class="logo">{safe_logo}</div>
810
+ <h1>{safe_title}</h1>
811
+ <p class="timestamp">Generated: {safe_timestamp}</p>
812
+ {subtitle_html}
813
+ </div>
814
+ {health_html}
815
+ </div>
816
+
817
+ {content}
818
+
819
+ <footer>{safe_footer}</footer>
820
+ </div>
821
+ </body>
822
+ </html>'''
823
+
824
+
825
+ # =============================================================================
826
+ # HTML Component Builders
827
+ # =============================================================================
828
+
829
+ def format_mutator_name(mutator):
830
+ import re
831
+ name = mutator.replace('Mutator', '').replace('Mutators', '')
832
+ name = re.sub(r'([a-z])([A-Z])', r'\1 \2', name)
833
+ replacements = {
834
+ 'CONDITIONALS BOUNDARY': 'Conditionals Boundary',
835
+ 'INCREMENTS': 'Increments',
836
+ 'INVERT NEGS': 'Invert Negatives',
837
+ 'MATH': 'Math Operations',
838
+ 'NEGATE CONDITIONALS': 'Negate Conditionals',
839
+ 'VOID METHOD CALLS': 'Void Method Calls',
840
+ 'RETURN VALS': 'Return Values',
841
+ 'NON VOID METHOD CALLS': 'Non-Void Method Calls',
842
+ 'CONSTRUCTOR CALLS': 'Constructor Calls',
843
+ 'INLINE CONSTANT': 'Inline Constants',
844
+ 'EMPTY RETURNS': 'Empty Returns',
845
+ 'FALSE RETURNS': 'False Returns',
846
+ 'TRUE RETURNS': 'True Returns',
847
+ 'NULL RETURNS': 'Null Returns',
848
+ 'PRIMITIVE RETURNS': 'Primitive Returns',
849
+ }
850
+ name_upper = name.upper().strip()
851
+ if name_upper in replacements:
852
+ return replacements[name_upper]
853
+ return name.strip().title()
854
+
855
+
856
+ def build_summary_card(value, label, sub_value=None, status=None, icon=None, is_health=False, health_class=None, health_desc=None):
857
+ classes = ['summary-card']
858
+ if status:
859
+ classes.append(esc(str(status)))
860
+ if is_health:
861
+ classes.append('health')
862
+ if health_class:
863
+ classes.append(esc(str(health_class)))
864
+
865
+ icon_html = f'<div class="icon">{esc(str(icon))}</div>' if icon else ''
866
+ sub_html = f'<div class="sub-value">{esc(str(sub_value))}</div>' if sub_value else ''
867
+
868
+ if is_health:
869
+ return f'''<div class="{' '.join(classes)}">
870
+ {icon_html}
871
+ <div class="health-label">{esc(str(value))}</div>
872
+ <div class="health-desc">{esc(str(health_desc or ''))}</div>
873
+ </div>'''
874
+ else:
875
+ return f'''<div class="{' '.join(classes)}">
876
+ <div class="value">{esc(str(value))}</div>
877
+ <div class="label">{esc(str(label))}</div>
878
+ {sub_html}
879
+ </div>'''
880
+
881
+
882
+ def build_coverage_item(name, value, percentage, details, color=None):
883
+ status_class = 'success' if percentage >= 80 else 'warning' if percentage >= 50 else 'error'
884
+ color = color or f"var(--color-{status_class})"
885
+ safe_pct = min(float(percentage), 100)
886
+ return f'''<div class="coverage-item">
887
+ <div class="header">
888
+ <span class="metric-name">{esc(str(name))}</span>
889
+ <span class="metric-value" style="color: {color};">{esc(str(value))}</span>
890
+ </div>
891
+ <div class="progress-bar">
892
+ <div class="progress-fill {status_class}" style="width: {safe_pct:.1f}%;"></div>
893
+ </div>
894
+ <div class="details">{esc(str(details))}</div>
895
+ </div>'''
896
+
897
+
898
+ def build_gaps_section(title, items, warning=False):
899
+ if not items:
900
+ return ''
901
+ section_class = 'gaps-section warning' if warning else 'gaps-section'
902
+
903
+ def gap_item(item):
904
+ method = esc(str(item["method"]))
905
+ path = esc(str(item["path"]))
906
+ count = item.get("count")
907
+ count_str = f" ({esc(str(count))} tests)" if count else ""
908
+ return f'<div class="gap-item"><span class="method method-{method}">{method}</span> {path}{count_str}</div>'
909
+
910
+ items_html = '\n'.join(gap_item(item) for item in items)
911
+ return f'''<div class="{section_class}">
912
+ <div class="title">{esc(str(title))}</div>
913
+ <div class="gap-list">{items_html}</div>
914
+ </div>'''
915
+
916
+
917
+ def build_collapsible_table(title, count, headers, rows):
918
+ if not rows:
919
+ return ''
920
+ headers_html = ''.join(f'<th style="{h.get("style", "")}">{h["label"]}</th>' for h in headers)
921
+ rows_html = '\n'.join(rows)
922
+ return f'''<details>
923
+ <summary>
924
+ {title}
925
+ <span class="count" style="background: var(--color-border); padding: 0.125rem 0.5rem; border-radius: 10px; font-size: 0.75rem; font-weight: 500;">{count}</span>
926
+ </summary>
927
+ <div class="content">
928
+ <table>
929
+ <thead><tr>{headers_html}</tr></thead>
930
+ <tbody>{rows_html}</tbody>
931
+ </table>
932
+ </div>
933
+ </details>'''
934
+
935
+
936
+ def build_endpoint_row(endpoint, stats):
937
+ test_count = stats.get('test_count', 0)
938
+ status_codes = stats.get('status_codes', set())
939
+ codes_str = ', '.join(sorted(status_codes, key=lambda x: int(x) if x.isdigit() else 0)) if status_codes else '—'
940
+ body = stats.get('body_assertions', 0) or '—'
941
+ has_2xx = any(str(c).startswith('2') for c in status_codes)
942
+ success_icon = '✅' if has_2xx else ('❌' if test_count > 0 else '—')
943
+
944
+ defined = endpoint.get('defined_params', set())
945
+ tested = stats.get('query_params', set())
946
+ if not defined:
947
+ qp_str = '—'
948
+ else:
949
+ tested_norm = set(p.lower().replace('_', '') for p in tested)
950
+ covered = len(defined.intersection(tested_norm))
951
+ qp_str = f'{covered}/{len(defined)}'
952
+
953
+ oneof = stats.get('oneof_count', 0)
954
+ weak_str = f'⚠️ {oneof}' if oneof > 0 else '—'
955
+
956
+ if test_count == 0:
957
+ icon, style = '❌', 'background: #fef2f2;'
958
+ elif test_count < 3:
959
+ icon, style = '⚠️', ''
960
+ else:
961
+ icon, style = '✅', ''
962
+
963
+ return f'''<tr style="{style}">
964
+ <td class="status-icon">{icon}</td>
965
+ <td><span class="method method-{endpoint['method']}">{endpoint['method']}</span></td>
966
+ <td class="left"><span class="path">{endpoint['path']}</span></td>
967
+ <td>{test_count}</td>
968
+ <td class="status-icon">{success_icon}</td>
969
+ <td>{codes_str}</td>
970
+ <td>{body}</td>
971
+ <td>{qp_str}</td>
972
+ <td>{weak_str}</td>
973
+ </tr>'''