cucumber-dressing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,528 @@
1
+ /**
2
+ * Template Generator - Creates HTML report from data
3
+ */
4
+ import { readFileSync } from 'fs';
5
+ import Handlebars from 'handlebars';
6
+ import * as path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import scriptsTemplate from './templates/scripts.js.template.js';
9
+ import stylesTemplate from './templates/styles.css.template.js';
10
+ import { extractErrorMessage, formatDuration, formatTimestamp, getStatusClass, getStatusIcon, markdownToHtml, parseTags, sanitizeHtml, } from './utils.js';
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const packageJson = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
14
+ export class TemplateGenerator {
15
+ options;
16
+ constructor(options) {
17
+ this.options = options;
18
+ this.registerHelpers();
19
+ }
20
+ /**
21
+ * Register Handlebars helpers for formatting, status handling, and data manipulation in templates
22
+ */
23
+ registerHelpers() {
24
+ Handlebars.registerHelper('formatDuration', (duration) => {
25
+ return formatDuration(duration, this.options.durationInMS);
26
+ });
27
+ Handlebars.registerHelper('statusIcon', (status) => {
28
+ return getStatusIcon(status);
29
+ });
30
+ Handlebars.registerHelper('statusClass', (status) => {
31
+ return getStatusClass(status);
32
+ });
33
+ Handlebars.registerHelper('sanitize', (text) => {
34
+ return new Handlebars.SafeString(sanitizeHtml(text));
35
+ });
36
+ Handlebars.registerHelper('markdown', (text) => {
37
+ return new Handlebars.SafeString(markdownToHtml(sanitizeHtml(text)));
38
+ });
39
+ Handlebars.registerHelper('parseTags', (tags) => {
40
+ return parseTags(tags).join(', ');
41
+ });
42
+ Handlebars.registerHelper('formatTimestamp', (timestamp) => {
43
+ return formatTimestamp(timestamp);
44
+ });
45
+ Handlebars.registerHelper('json', (context) => {
46
+ return JSON.stringify(context, null, 2);
47
+ });
48
+ Handlebars.registerHelper('percentage', (value, total) => {
49
+ if (total === 0) {
50
+ return '0';
51
+ }
52
+ return ((value / total) * 100).toFixed(1);
53
+ });
54
+ Handlebars.registerHelper('eq', (a, b) => {
55
+ return a === b;
56
+ });
57
+ Handlebars.registerHelper('or', (...args) => {
58
+ return args.slice(0, -1).some(arg => !!arg);
59
+ });
60
+ Handlebars.registerHelper('and', (...args) => {
61
+ return args.slice(0, -1).every(arg => !!arg);
62
+ });
63
+ Handlebars.registerHelper('gt', (a, b) => {
64
+ return a > b;
65
+ });
66
+ Handlebars.registerHelper('extractError', (result) => {
67
+ return extractErrorMessage(result);
68
+ });
69
+ Handlebars.registerHelper('hasEmbedding', (step, type) => {
70
+ if (!step.embeddings) {
71
+ return false;
72
+ }
73
+ return step.embeddings.some(e => e.mime_type.includes(type));
74
+ });
75
+ Handlebars.registerHelper('getEmbedding', (step, type) => {
76
+ if (!step.embeddings) {
77
+ return null;
78
+ }
79
+ return step.embeddings.find(e => e.mime_type.includes(type));
80
+ });
81
+ }
82
+ /**
83
+ * Generate complete HTML report from processed data
84
+ * @param features - Processed features with calculated status and duration
85
+ * @param statistics - Report statistics including counts and pass rate
86
+ * @param chartData - Chart.js data for visualization
87
+ * @returns Complete HTML report as a string
88
+ */
89
+ generateReport(features, statistics, chartData) {
90
+ const template = this.getMainTemplate();
91
+ const compiled = Handlebars.compile(template);
92
+ const data = {
93
+ reportTitle: this.options.reportTitle ?? 'DRESSING Test Report',
94
+ reportName: this.options.reportName ?? 'Test Execution Report',
95
+ brandTitle: this.options.brandTitle ?? 'DRESSING',
96
+ theme: this.options.theme ?? 'modern',
97
+ features,
98
+ statistics,
99
+ chartData,
100
+ metadata: this.options.metadata,
101
+ customData: this.options.customData,
102
+ displayDuration: this.options.displayDuration !== false,
103
+ scenarioTimestamp: this.options.scenarioTimestamp,
104
+ columnLayout: this.options.columnLayout ?? 2,
105
+ generatedAt: formatTimestamp(Date.now()),
106
+ };
107
+ return compiled(data);
108
+ }
109
+ /**
110
+ * Get main HTML template structure with embedded styles and scripts
111
+ * @returns HTML template string with placeholders for Handlebars compilation
112
+ */
113
+ getMainTemplate() {
114
+ return `<!DOCTYPE html>
115
+ <html lang="en">
116
+ <head>
117
+ <meta charset="UTF-8">
118
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
119
+ <title>{{reportTitle}}</title>
120
+ <style>
121
+ ${this.getStyles()}
122
+ ${this.getCustomColorStyles()}
123
+ </style>
124
+ </head>
125
+ <body class="theme-{{theme}}">
126
+ <div class="container">
127
+ ${this.getHeaderTemplate()}
128
+ ${this.getStatisticsTemplate()}
129
+ ${this.getChartsTemplate()}
130
+ ${this.getMetadataTemplate()}
131
+ ${this.getFeaturesListTemplate()}
132
+ ${this.getFooterTemplate()}
133
+ </div>
134
+ <script>
135
+ ${this.getScripts()}
136
+ </script>
137
+ </body>
138
+ </html>`;
139
+ }
140
+ /**
141
+ * Get header template with branding and report info
142
+ * @returns HTML template string for the report header section
143
+ */
144
+ getHeaderTemplate() {
145
+ return `
146
+ <header class="report-header">
147
+ <div class="brand">
148
+ <h1>{{brandTitle}}</h1>
149
+ <span class="tagline">Detailed Report of Executed Scenarios, Steps and INsights for Gherkin</span>
150
+ </div>
151
+ <div class="report-info">
152
+ <h2>{{reportName}}</h2>
153
+ <p class="generated-time">Generated: {{generatedAt}}</p>
154
+ </div>
155
+ </header>
156
+ `;
157
+ }
158
+ /**
159
+ * Get statistics overview template with test execution summary cards
160
+ * @returns HTML template string for statistics display
161
+ */
162
+ getStatisticsTemplate() {
163
+ return `
164
+ <section class="statistics-overview">
165
+ <h3>Test Execution Summary</h3>
166
+ <div class="stats-grid">
167
+ <div class="stat-card">
168
+ <div class="stat-value">{{statistics.features}}</div>
169
+ <div class="stat-label">Features</div>
170
+ </div>
171
+ <div class="stat-card">
172
+ <div class="stat-value">{{statistics.scenarios}}</div>
173
+ <div class="stat-label">Scenarios</div>
174
+ </div>
175
+ <div class="stat-card">
176
+ <div class="stat-value">{{statistics.steps}}</div>
177
+ <div class="stat-label">Steps</div>
178
+ </div>
179
+ <div class="stat-card status-passed">
180
+ <div class="stat-value">{{statistics.passed}}</div>
181
+ <div class="stat-label">Passed</div>
182
+ </div>
183
+ <div class="stat-card status-failed">
184
+ <div class="stat-value">{{statistics.failed}}</div>
185
+ <div class="stat-label">Failed</div>
186
+ </div>
187
+ <div class="stat-card status-skipped">
188
+ <div class="stat-value">{{statistics.skipped}}</div>
189
+ <div class="stat-label">Skipped</div>
190
+ </div>
191
+ {{#if displayDuration}}
192
+ <div class="stat-card">
193
+ <div class="stat-value">{{formatDuration statistics.duration}}</div>
194
+ <div class="stat-label">Duration</div>
195
+ </div>
196
+ {{/if}}
197
+ <div class="stat-card">
198
+ <div class="stat-value">{{statistics.passRate}}%</div>
199
+ <div class="stat-label">Pass Rate</div>
200
+ </div>
201
+ </div>
202
+ </section>
203
+ `;
204
+ }
205
+ /**
206
+ * Get charts section template for status distribution and pass rate visualization
207
+ * @returns HTML template string with canvas elements for Chart.js
208
+ */
209
+ getChartsTemplate() {
210
+ return `
211
+ <section class="charts-section">
212
+ <div class="charts-grid">
213
+ <div class="chart-container">
214
+ <h4>Status Distribution</h4>
215
+ <canvas id="statusChart"></canvas>
216
+ </div>
217
+ <div class="chart-container">
218
+ <h4>Pass Rate</h4>
219
+ <canvas id="passRateChart"></canvas>
220
+ </div>
221
+ </div>
222
+ </section>
223
+ `;
224
+ }
225
+ /**
226
+ * Get metadata section template for browser, platform, and custom data display
227
+ * @returns HTML template string for optional metadata sections
228
+ */
229
+ getMetadataTemplate() {
230
+ return `
231
+ {{#if metadata}}
232
+ <section class="metadata-section">
233
+ <h3>Test Metadata</h3>
234
+ <div class="metadata-grid">
235
+ {{#if metadata.browser}}
236
+ <div class="metadata-item">
237
+ <span class="metadata-label">Browser:</span>
238
+ <span class="metadata-value">{{metadata.browser.name}} {{metadata.browser.version}}</span>
239
+ </div>
240
+ {{/if}}
241
+ {{#if metadata.platform}}
242
+ <div class="metadata-item">
243
+ <span class="metadata-label">Platform:</span>
244
+ <span class="metadata-value">{{metadata.platform.name}} {{metadata.platform.version}}</span>
245
+ </div>
246
+ {{/if}}
247
+ {{#if metadata.device}}
248
+ <div class="metadata-item">
249
+ <span class="metadata-label">Device:</span>
250
+ <span class="metadata-value">{{metadata.device}}</span>
251
+ </div>
252
+ {{/if}}
253
+ </div>
254
+ </section>
255
+ {{/if}}
256
+
257
+ {{#if customData}}
258
+ <section class="custom-data-section">
259
+ <h3>{{customData.title}}</h3>
260
+ <div class="custom-data-grid">
261
+ {{#each customData.data}}
262
+ <div class="custom-data-item">
263
+ <span class="custom-data-label">{{this.label}}:</span>
264
+ <span class="custom-data-value">{{this.value}}</span>
265
+ </div>
266
+ {{/each}}
267
+ </div>
268
+ </section>
269
+ {{/if}}
270
+ `;
271
+ }
272
+ /**
273
+ * Get features list template with search filter and feature cards
274
+ * @returns HTML template string for features section
275
+ */
276
+ getFeaturesListTemplate() {
277
+ return `
278
+ <section class="features-section">
279
+ <h3>Features</h3>
280
+ ${this.getFilterTemplate()}
281
+ <div class="features-list">
282
+ {{#each features}}
283
+ ${this.getFeatureCardTemplate()}
284
+ {{/each}}
285
+ </div>
286
+ </section>
287
+ `;
288
+ }
289
+ /**
290
+ * Get filter controls template for searching and status filtering
291
+ * @returns HTML template string with search input and filter buttons
292
+ */
293
+ getFilterTemplate() {
294
+ return `
295
+ <div class="features-filter">
296
+ <input type="text" id="searchFilter" placeholder="Search features, scenarios, or tags..." />
297
+ <div class="status-filters">
298
+ <button class="filter-btn active" data-status="all">All</button>
299
+ <button class="filter-btn" data-status="passed">Passed</button>
300
+ <button class="filter-btn" data-status="failed">Failed</button>
301
+ <button class="filter-btn" data-status="skipped">Skipped</button>
302
+ </div>
303
+ </div>
304
+ `;
305
+ }
306
+ /**
307
+ * Get feature card template with collapsible scenarios
308
+ * @returns HTML template string for individual feature display
309
+ */
310
+ getFeatureCardTemplate() {
311
+ return `
312
+ <div class="feature-card {{statusClass this.status}}" data-status="{{this.status}}">
313
+ ${this.getFeatureHeaderTemplate()}
314
+ {{#if this.description}}
315
+ <div class="feature-description">{{markdown this.description}}</div>
316
+ {{/if}}
317
+ <div class="feature-content" id="feature-{{@index}}" style="display: none;">
318
+ <div class="scenarios-list">
319
+ {{#each this.scenarios}}
320
+ ${this.getScenarioCardTemplate()}
321
+ {{/each}}
322
+ </div>
323
+ </div>
324
+ </div>
325
+ `;
326
+ }
327
+ /**
328
+ * Get feature header template with title, tags, and toggle controls
329
+ * @returns HTML template string for feature header with collapsible functionality
330
+ */
331
+ getFeatureHeaderTemplate() {
332
+ return `
333
+ <div class="feature-header" onclick="toggleFeature('feature-{{@index}}')">
334
+ <div class="feature-title">
335
+ <span class="status-icon">{{statusIcon this.status}}</span>
336
+ <h4>{{this.name}}</h4>
337
+ {{#if this.tags}}
338
+ <div class="tags">
339
+ {{#each this.tags}}
340
+ <span class="tag">{{this.name}}</span>
341
+ {{/each}}
342
+ </div>
343
+ {{/if}}
344
+ </div>
345
+ <div class="feature-stats">
346
+ <span class="stat">{{this.scenarios.length}} scenarios</span>
347
+ {{#if ../displayDuration}}
348
+ <span class="stat">{{formatDuration this.duration}}</span>
349
+ {{/if}}
350
+ <span class="toggle-icon">▼</span>
351
+ </div>
352
+ </div>
353
+ `;
354
+ }
355
+ /**
356
+ * Get scenario card template with collapsible steps
357
+ * @returns HTML template string for individual scenario display
358
+ */
359
+ getScenarioCardTemplate() {
360
+ return `
361
+ <div class="scenario-card {{statusClass this.status}}">
362
+ ${this.getScenarioHeaderTemplate()}
363
+ <div class="scenario-content" id="scenario-{{../id}}-{{@index}}" style="display: none;">
364
+ {{#if this.description}}
365
+ <div class="scenario-description">{{markdown this.description}}</div>
366
+ {{/if}}
367
+ ${this.getStepsListTemplate()}
368
+ </div>
369
+ </div>
370
+ `;
371
+ }
372
+ /**
373
+ * Get scenario header template with title, tags, and toggle controls
374
+ * @returns HTML template string for scenario header with collapsible functionality
375
+ */
376
+ getScenarioHeaderTemplate() {
377
+ return `
378
+ <div class="scenario-header" onclick="toggleScenario('scenario-{{../id}}-{{@index}}')">
379
+ <div class="scenario-title">
380
+ <span class="status-icon">{{statusIcon this.status}}</span>
381
+ <h5>{{this.keyword}}: {{this.name}}</h5>
382
+ {{#if this.tags}}
383
+ <div class="tags">
384
+ {{#each this.tags}}
385
+ <span class="tag">{{this.name}}</span>
386
+ {{/each}}
387
+ </div>
388
+ {{/if}}
389
+ </div>
390
+ <div class="scenario-stats">
391
+ {{#if ../../displayDuration}}
392
+ <span class="stat">{{formatDuration this.duration}}</span>
393
+ {{/if}}
394
+ <span class="toggle-icon">▼</span>
395
+ </div>
396
+ </div>
397
+ `;
398
+ }
399
+ /**
400
+ * Get steps list template with errors, data tables, and screenshots
401
+ * @returns HTML template string for scenario steps display
402
+ */
403
+ getStepsListTemplate() {
404
+ return `
405
+ <div class="steps-list">
406
+ {{#each this.steps}}
407
+ <div class="step-item {{statusClass this.result.status}}">
408
+ <div class="step-header">
409
+ <span class="status-icon">{{statusIcon this.result.status}}</span>
410
+ <span class="step-keyword">{{this.keyword}}</span>
411
+ <span class="step-name">{{this.name}}</span>
412
+ {{#if ../../../displayDuration}}
413
+ <span class="step-duration">{{formatDuration this.result.duration}}</span>
414
+ {{/if}}
415
+ </div>
416
+ {{#if this.result.error_message}}
417
+ <div class="step-error">
418
+ <pre>{{this.result.error_message}}</pre>
419
+ </div>
420
+ {{/if}}
421
+ {{#if this.doc_string}}
422
+ <div class="step-doc-string">
423
+ <pre>{{this.doc_string.value}}</pre>
424
+ </div>
425
+ {{/if}}
426
+ {{#if this.rows}}
427
+ <div class="step-data-table">
428
+ <table>
429
+ {{#each this.rows}}
430
+ <tr>
431
+ {{#each this.cells}}
432
+ <td>{{this}}</td>
433
+ {{/each}}
434
+ </tr>
435
+ {{/each}}
436
+ </table>
437
+ </div>
438
+ {{/if}}
439
+ {{#if (hasEmbedding this "image")}}
440
+ <div class="step-screenshot">
441
+ <img src="data:{{getEmbedding this "image"}}.mime_type;base64,{{getEmbedding this "image"}}.data" alt="Screenshot" />
442
+ </div>
443
+ {{/if}}
444
+ </div>
445
+ {{/each}}
446
+ </div>
447
+ `;
448
+ }
449
+ /**
450
+ * Get footer template with branding and version info
451
+ * @returns HTML template string for report footer
452
+ */
453
+ getFooterTemplate() {
454
+ return `
455
+ <footer class="report-footer">
456
+ <p>Generated by <strong>DRESSING</strong> - Detailed Report of Executed Scenarios, Steps and INsights for Gherkin</p>
457
+ <p class="version">v${packageJson.version}</p>
458
+ </footer>
459
+ `;
460
+ }
461
+ /**
462
+ * Get CSS styles from template file
463
+ * @returns CSS stylesheet string for report styling
464
+ */
465
+ getStyles() {
466
+ return stylesTemplate;
467
+ }
468
+ /**
469
+ * Generate custom CSS color variables if colors option is provided
470
+ * @returns CSS custom properties string with color overrides, or empty string if no colors specified
471
+ */
472
+ getCustomColorStyles() {
473
+ if (!this.options.colors) {
474
+ return '';
475
+ }
476
+ const colors = this.options.colors;
477
+ const cssVars = [];
478
+ if (colors.primary) {
479
+ cssVars.push(`--color-primary: ${colors.primary};`);
480
+ }
481
+ if (colors.success) {
482
+ cssVars.push(`--color-success: ${colors.success};`);
483
+ }
484
+ if (colors.danger) {
485
+ cssVars.push(`--color-danger: ${colors.danger};`);
486
+ }
487
+ if (colors.warning) {
488
+ cssVars.push(`--color-warning: ${colors.warning};`);
489
+ }
490
+ if (colors.info) {
491
+ cssVars.push(`--color-info: ${colors.info};`);
492
+ }
493
+ if (colors.muted) {
494
+ cssVars.push(`--color-muted: ${colors.muted};`);
495
+ }
496
+ if (colors.background) {
497
+ cssVars.push(`--color-bg: ${colors.background};`);
498
+ }
499
+ if (colors.backgroundSecondary) {
500
+ cssVars.push(`--color-bg-secondary: ${colors.backgroundSecondary};`);
501
+ }
502
+ if (colors.border) {
503
+ cssVars.push(`--color-border: ${colors.border};`);
504
+ }
505
+ if (colors.text) {
506
+ cssVars.push(`--color-text: ${colors.text};`);
507
+ }
508
+ if (colors.textSecondary) {
509
+ cssVars.push(`--color-text-secondary: ${colors.textSecondary};`);
510
+ }
511
+ if (cssVars.length === 0) {
512
+ return '';
513
+ }
514
+ return `
515
+ :root {
516
+ ${cssVars.join('\n ')}
517
+ }
518
+ `;
519
+ }
520
+ /**
521
+ * Get JavaScript code from template file for report interactivity
522
+ * @returns JavaScript code string for charts, filtering, and collapsible sections
523
+ */
524
+ getScripts() {
525
+ return scriptsTemplate;
526
+ }
527
+ }
528
+ //# sourceMappingURL=template-generator.js.map
@@ -0,0 +1,3 @@
1
+ declare const _default: "\n// DRESSING - JavaScript for interactivity\n\n// Toggle feature visibility\nfunction toggleFeature(id) {\n const element = document.getElementById(id);\n const card = element.closest('.feature-card');\n\n if (element.style.display === 'none') {\n element.style.display = 'block';\n card.classList.add('expanded');\n } else {\n element.style.display = 'none';\n card.classList.remove('expanded');\n }\n}\n\n// Toggle scenario visibility\nfunction toggleScenario(id) {\n const element = document.getElementById(id);\n const card = element.closest('.scenario-card');\n\n if (element.style.display === 'none') {\n element.style.display = 'block';\n card.classList.add('expanded');\n } else {\n element.style.display = 'none';\n card.classList.remove('expanded');\n }\n}\n\n// Search and filter functionality\ndocument.addEventListener('DOMContentLoaded', function() {\n const searchInput = document.getElementById('searchFilter');\n const filterButtons = document.querySelectorAll('.filter-btn');\n const featureCards = document.querySelectorAll('.feature-card');\n\n let currentStatusFilter = 'all';\n\n // Search functionality\n if (searchInput) {\n searchInput.addEventListener('input', function(e) {\n const searchTerm = e.target.value.toLowerCase();\n filterFeatures(searchTerm, currentStatusFilter);\n });\n }\n\n // Status filter functionality\n filterButtons.forEach(button => {\n button.addEventListener('click', function() {\n filterButtons.forEach(btn => btn.classList.remove('active'));\n this.classList.add('active');\n currentStatusFilter = this.dataset.status;\n\n const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';\n filterFeatures(searchTerm, currentStatusFilter);\n });\n });\n\n // Filter features based on search and status\n function filterFeatures(searchTerm, statusFilter) {\n featureCards.forEach(card => {\n const featureName = card.querySelector('.feature-title h4')?.textContent.toLowerCase() || '';\n const tags = Array.from(card.querySelectorAll('.tag')).map(tag => tag.textContent.toLowerCase());\n const scenarios = Array.from(card.querySelectorAll('.scenario-title h5')).map(s => s.textContent.toLowerCase());\n const status = card.dataset.status;\n\n const matchesSearch = !searchTerm ||\n featureName.includes(searchTerm) ||\n tags.some(tag => tag.includes(searchTerm)) ||\n scenarios.some(scenario => scenario.includes(searchTerm));\n\n const matchesStatus = statusFilter === 'all' || status === statusFilter;\n\n if (matchesSearch && matchesStatus) {\n card.style.display = 'block';\n } else {\n card.style.display = 'none';\n }\n });\n }\n\n // Expand failed features by default\n expandFailedFeatures();\n});\n\n// Initialize Chart.js charts\nfunction initializeCharts() {\n // Status Distribution Chart\n const statusChartCanvas = document.getElementById('statusChart');\n if (statusChartCanvas) {\n const ctx = statusChartCanvas.getContext('2d');\n\n // Get data from the page\n const statsCards = document.querySelectorAll('.stat-card');\n let passed = 0, failed = 0, skipped = 0;\n\n statsCards.forEach(card => {\n const label = card.querySelector('.stat-label')?.textContent.toLowerCase();\n const value = parseInt(card.querySelector('.stat-value')?.textContent) || 0;\n\n if (label === 'passed') passed = value;\n if (label === 'failed') failed = value;\n if (label === 'skipped') skipped = value;\n });\n\n new Chart(ctx, {\n type: 'doughnut',\n data: {\n labels: ['Passed', 'Failed', 'Skipped'],\n datasets: [{\n data: [passed, failed, skipped],\n backgroundColor: [\n 'rgba(16, 185, 129, 0.8)',\n 'rgba(239, 68, 68, 0.8)',\n 'rgba(245, 158, 11, 0.8)'\n ],\n borderColor: [\n 'rgb(16, 185, 129)',\n 'rgb(239, 68, 68)',\n 'rgb(245, 158, 11)'\n ],\n borderWidth: 2\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: true,\n plugins: {\n legend: {\n position: 'bottom',\n },\n tooltip: {\n callbacks: {\n label: function(context) {\n const label = context.label || '';\n const value = context.parsed || 0;\n const total = context.dataset.data.reduce((a, b) => a + b, 0);\n const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;\n return label + ': ' + value + ' (' + percentage + '%)';\n }\n }\n }\n }\n }\n });\n }\n\n // Pass Rate Chart\n const passRateChartCanvas = document.getElementById('passRateChart');\n if (passRateChartCanvas) {\n const ctx = passRateChartCanvas.getContext('2d');\n\n // Get pass rate from the page\n const passRateCard = Array.from(document.querySelectorAll('.stat-card')).find(card =>\n card.querySelector('.stat-label')?.textContent.toLowerCase() === 'pass rate'\n );\n\n const passRate = passRateCard ? parseFloat(passRateCard.querySelector('.stat-value')?.textContent) : 0;\n const failRate = 100 - passRate;\n\n new Chart(ctx, {\n type: 'pie',\n data: {\n labels: ['Passed', 'Failed'],\n datasets: [{\n data: [passRate, failRate],\n backgroundColor: [\n 'rgba(16, 185, 129, 0.8)',\n 'rgba(239, 68, 68, 0.8)'\n ],\n borderColor: [\n 'rgb(16, 185, 129)',\n 'rgb(239, 68, 68)'\n ],\n borderWidth: 2\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: true,\n plugins: {\n legend: {\n position: 'bottom',\n },\n tooltip: {\n callbacks: {\n label: function(context) {\n const label = context.label || '';\n const value = context.parsed || 0;\n return label + ': ' + value.toFixed(1) + '%';\n }\n }\n }\n }\n }\n });\n }\n}\n\n// Expand failed features automatically\nfunction expandFailedFeatures() {\n const failedFeatures = document.querySelectorAll('.feature-card.status-failed');\n failedFeatures.forEach(feature => {\n const featureContent = feature.querySelector('.feature-content');\n if (featureContent) {\n featureContent.style.display = 'block';\n feature.classList.add('expanded');\n\n // Also expand failed scenarios within\n const failedScenarios = feature.querySelectorAll('.scenario-card.status-failed');\n failedScenarios.forEach(scenario => {\n const scenarioContent = scenario.querySelector('.scenario-content');\n if (scenarioContent) {\n scenarioContent.style.display = 'block';\n scenario.classList.add('expanded');\n }\n });\n }\n });\n}\n\n// Keyboard shortcuts\ndocument.addEventListener('keydown', function(e) {\n // Ctrl/Cmd + F to focus search\n if ((e.ctrlKey || e.metaKey) && e.key === 'f') {\n e.preventDefault();\n const searchInput = document.getElementById('searchFilter');\n if (searchInput) {\n searchInput.focus();\n }\n }\n\n // Escape to clear search\n if (e.key === 'Escape') {\n const searchInput = document.getElementById('searchFilter');\n if (searchInput && document.activeElement === searchInput) {\n searchInput.value = '';\n searchInput.dispatchEvent(new Event('input'));\n }\n }\n\n // Ctrl/Cmd + E to expand all\n if ((e.ctrlKey || e.metaKey) && e.key === 'e') {\n e.preventDefault();\n expandAll();\n }\n\n // Ctrl/Cmd + C to collapse all\n if ((e.ctrlKey || e.metaKey) && e.key === 'c') {\n e.preventDefault();\n collapseAll();\n }\n});\n\n// Expand all features and scenarios\nfunction expandAll() {\n document.querySelectorAll('.feature-content').forEach(content => {\n content.style.display = 'block';\n content.closest('.feature-card')?.classList.add('expanded');\n });\n\n document.querySelectorAll('.scenario-content').forEach(content => {\n content.style.display = 'block';\n content.closest('.scenario-card')?.classList.add('expanded');\n });\n}\n\n// Collapse all features and scenarios\nfunction collapseAll() {\n document.querySelectorAll('.feature-content').forEach(content => {\n content.style.display = 'none';\n content.closest('.feature-card')?.classList.remove('expanded');\n });\n\n document.querySelectorAll('.scenario-content').forEach(content => {\n content.style.display = 'none';\n content.closest('.scenario-card')?.classList.remove('expanded');\n });\n}\n\n// Add Chart.js CDN and initialize charts after loading\n(function() {\n const script = document.createElement('script');\n script.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js';\n script.onload = function() {\n console.log('Chart.js loaded successfully');\n // Initialize charts after Chart.js is loaded\n initializeCharts();\n };\n script.onerror = function() {\n console.error('Failed to load Chart.js');\n };\n document.head.appendChild(script);\n})();\n";
2
+ export default _default;
3
+ //# sourceMappingURL=scripts.js.template.d.ts.map