slik-report 1.0.0 → 1.0.2

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.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # slik-report
2
+
3
+ A powerful Playwright reporter for aggregating test results, flakiness analysis, and historical trend tracking.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install slik-report --save-dev
9
+ ```
10
+
11
+ ## How to Use
12
+
13
+ To use `slik-report` with Playwright, you need to configure it in your `playwright.config.ts` file.
14
+
15
+ ### Configuration Example
16
+
17
+ Add `slik-report` to the `reporter` array in your `playwright.config.ts`:
18
+
19
+ ```typescript
20
+ // playwright.config.ts
21
+ import { defineConfig } from '@playwright/test';
22
+
23
+ export default defineConfig({
24
+ reporter: [
25
+ ['json', { outputFile: 'playwright-report/report.json' }],
26
+ // Use the Slik Report reporter and pass options
27
+ ['slik-report', {
28
+ input: 'playwright-report/report.json', // Path to the JSON report generated by Playwright
29
+ output: 'slik-report.html', // Output file name for the Slik report
30
+ history: './slik_history', // Directory to store history data
31
+ }]
32
+ ],
33
+ });
34
+ ```
35
+
36
+ ### Options
37
+
38
+ | Option | Type | Description | Default |
39
+ | :-------- | :----- | :----------------------------------------------- | :-------------------------- |
40
+ | `input` | string | Path to the JSON report file generated by Playwright. | `playwright-report/report.json` |
41
+ | `output` | string | Path where the HTML report will be generated. | `slik-report.html` |
42
+ | `history` | string | Directory path to store historical run data. | `./slik_history` |
43
+
44
+ ## License
45
+
46
+ ISC
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js CHANGED
@@ -1,3 +1,984 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=index.js.map
3
+ // @ts-nocheck
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ function ensureDir(p) {
7
+ if (!fs.existsSync(p))
8
+ fs.mkdirSync(p, { recursive: true });
9
+ }
10
+ function writeFile(filePath, content) {
11
+ ensureDir(path.dirname(filePath));
12
+ fs.writeFileSync(filePath, content, 'utf8');
13
+ }
14
+ function safeJson(v) {
15
+ try {
16
+ return JSON.stringify(v);
17
+ }
18
+ catch (e) {
19
+ return '[]';
20
+ }
21
+ }
22
+ function stripAnsi(str) {
23
+ if (!str)
24
+ return '';
25
+ const ansiRegex = /[\u001b\u009b][[()#;?]*.{0,2}(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
26
+ return str.replace(ansiRegex, '');
27
+ }
28
+ /**
29
+ * Escapes a JSON string for safe embedding within an HTML <script> tag.
30
+ * This prevents the JSON from prematurely closing the script block (e.g., if it contains </script>).
31
+ */
32
+ function escapeJsonForHtml(jsonString) {
33
+ return jsonString.replace(/<\/script>/g, '<\\/script>');
34
+ }
35
+ class AggregatorReporter {
36
+ constructor(opts) {
37
+ this.options = opts || {};
38
+ this.input = (this.options.input) || './playwright-report/report.json';
39
+ this.outputHtml = (this.options.output) || './summary-report.html';
40
+ this.historyDir = (this.options.history) || './reports/reports_history';
41
+ }
42
+ async onEnd(result) {
43
+ try {
44
+ console.log('AggregatorReporter: Starting report generation...');
45
+ if (!fs.existsSync(this.input)) {
46
+ console.warn('AggregatorReporter: JSON report not found at', this.input);
47
+ return;
48
+ }
49
+ console.log('AggregatorReporter: JSON file found. Reading file...');
50
+ const report = JSON.parse(fs.readFileSync(this.input, 'utf8'));
51
+ console.log('AggregatorReporter: JSON data read. Processing and generating report...');
52
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
53
+ const historyFile = path.join(this.historyDir, `run-${timestamp}.json`);
54
+ writeFile(historyFile, safeJson(report));
55
+ const history = await this.loadHistory();
56
+ const processedReport = this.processReportWithHistory(report, history);
57
+ const htmlContent = this.generateReportHtml(processedReport);
58
+ writeFile(this.outputHtml, htmlContent);
59
+ console.log('AggregatorReporter: Report generation successful!');
60
+ }
61
+ catch (e) {
62
+ console.error('AggregatorReporter failed:', e);
63
+ }
64
+ }
65
+ /**
66
+ * Helper function to robustly get the array of top-level test suites.
67
+ * This handles standard Playwright and various custom/merged report formats.
68
+ */
69
+ getTestSuites(report) {
70
+ if (!report)
71
+ return [];
72
+ // 1. Standard Playwright structure
73
+ if (report.suites && Array.isArray(report.suites)) {
74
+ return report.suites;
75
+ }
76
+ // 2. Common alternative in merged reports (a flat array of specs)
77
+ if (report.specs && Array.isArray(report.specs) && report.specs.length > 0) {
78
+ console.log('AggregatorReporter: Using flat "specs" array as report root.');
79
+ return [{ title: 'Root Suite (Flat Specs)', suites: [], specs: report.specs, file: 'N/A' }];
80
+ }
81
+ // 3. Check for a Playwright V2/aggregator structure where tests are flatly listed
82
+ if (report.tests && Array.isArray(report.tests) && report.tests.length > 0) {
83
+ console.log('AggregatorReporter: Using flat "tests" array as report root. (Playwright V2 or custom)');
84
+ return [{
85
+ title: 'Root Suite (Flat Tests)',
86
+ suites: [],
87
+ specs: report.tests.map(t => ({
88
+ title: t.title,
89
+ file: t.file,
90
+ tests: [t]
91
+ })),
92
+ file: 'N/A'
93
+ }];
94
+ }
95
+ if (report.stats) {
96
+ console.warn('AggregatorReporter: Could not find "suites" or alternative test structure. Returning empty suites list.');
97
+ }
98
+ return [];
99
+ }
100
+ findAllSpecs(suites) {
101
+ let specs = [];
102
+ if (!suites)
103
+ return specs;
104
+ for (const suite of suites) {
105
+ if (suite.specs)
106
+ specs.push(...suite.specs);
107
+ if (suite.suites) {
108
+ specs.push(...this.findAllSpecs(suite.suites));
109
+ }
110
+ }
111
+ return specs;
112
+ }
113
+ async loadHistory(n = 21) {
114
+ const history = [];
115
+ ensureDir(this.historyDir);
116
+ const files = fs.readdirSync(this.historyDir)
117
+ .filter(f => f.startsWith('run-') && f.endsWith('.json'))
118
+ .sort()
119
+ .slice(-n);
120
+ for (const file of files) {
121
+ try {
122
+ const filePath = path.join(this.historyDir, file);
123
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
124
+ const dateStr = file.replace('run-', '').replace('.json', '');
125
+ // The file name format is ISO-like but uses hyphens/underscores instead of colons/dots. We reconstruct a valid ISO string.
126
+ const validIso = dateStr.slice(0, 10) + 'T' +
127
+ dateStr.slice(11, 13) + ':' +
128
+ dateStr.slice(14, 16) + ':' +
129
+ dateStr.slice(17, 19) + '.' +
130
+ dateStr.slice(20) + 'Z';
131
+ history.push({
132
+ date: new Date(validIso).toLocaleDateString(),
133
+ report: data
134
+ });
135
+ }
136
+ catch (e) {
137
+ console.warn(`Could not read history file: ${file}`, e);
138
+ }
139
+ }
140
+ return history;
141
+ }
142
+ processReportWithHistory(report, history) {
143
+ const stats = this.processStats(report);
144
+ const suites = this.processSuites(report, history);
145
+ const categories = this.processCategories(report);
146
+ const historyData = this.processHistoryData(history, report);
147
+ const timeline = this.processTimeline(report);
148
+ const metadata = this.processMetadata(report);
149
+ return {
150
+ stats,
151
+ suites,
152
+ categories,
153
+ history: historyData,
154
+ timeline,
155
+ metadata
156
+ };
157
+ }
158
+ processStats(report) {
159
+ const stats = report.stats || {};
160
+ const { expected = 0, unexpected = 0, skipped = 0, duration = 0 } = stats;
161
+ const total = expected + unexpected + skipped;
162
+ const passRate = total > 0 ? `${((expected / total) * 100).toFixed(0)}%` : 'N/A';
163
+ const avgDuration = total > 0 ? `${(duration / total / 1000).toFixed(2)}s` : 'N/A';
164
+ return {
165
+ total,
166
+ passed: expected,
167
+ failed: unexpected,
168
+ skipped,
169
+ passRate,
170
+ avgDuration,
171
+ totalDuration: `${(duration / 1000).toFixed(2)}s`
172
+ };
173
+ }
174
+ processMetadata(report) {
175
+ // Safely access project name from the projects array
176
+ const projectName = report.config?.projects?.[0]?.name || report.config?.metadata?.project || 'N/A';
177
+ return {
178
+ browser: projectName,
179
+ os: report.config?.metadata?.platform || 'N/A', // Using 'platform' if available in ortoni/custom meta
180
+ startTime: report.stats?.startTime ? new Date(report.stats.startTime).toLocaleString() : 'N/A',
181
+ totalDuration: report.stats?.duration ? `${(report.stats.duration / 1000).toFixed(2)}s` : 'N/A'
182
+ };
183
+ }
184
+ processSuites(report, history) {
185
+ const historyDataMap = new Map();
186
+ history.forEach(run => {
187
+ const allSpecs = this.findAllSpecs(this.getTestSuites(run.report));
188
+ allSpecs.forEach(spec => {
189
+ const testId = `${spec.file}::${spec.title}`;
190
+ if (!historyDataMap.has(testId)) {
191
+ historyDataMap.set(testId, []);
192
+ }
193
+ // Safely get status, prioritizing unexpected over passed/skipped if multiple results exist
194
+ const resultStatus = spec.tests?.[0]?.results?.[0]?.status || 'skipped';
195
+ historyDataMap.get(testId).push(resultStatus);
196
+ });
197
+ });
198
+ const finalSuites = [];
199
+ const traverseSuites = (suite, parentTitle = '') => {
200
+ const currentTitle = parentTitle ? `${parentTitle} > ${suite.title}` : suite.title;
201
+ if (suite.specs && suite.specs.length > 0) {
202
+ finalSuites.push({
203
+ ...suite,
204
+ title: currentTitle,
205
+ specs: suite.specs.map(spec => {
206
+ const testId = `${spec.file}::${spec.title}`;
207
+ const historyStatuses = historyDataMap.get(testId) || [];
208
+ const currentTest = spec.tests?.[0];
209
+ const currentResult = currentTest?.results?.[0];
210
+ const currentStatus = currentResult?.status || 'skipped';
211
+ // We only want the history statuses from previous runs + the current one
212
+ const lastRuns = historyStatuses.slice(-4);
213
+ // Ensure the current run is included, and limit to 5 total runs
214
+ if (lastRuns.length === 0 || lastRuns[lastRuns.length - 1] !== currentStatus) {
215
+ lastRuns.push(currentStatus);
216
+ }
217
+ else if (lastRuns.length > 5) {
218
+ lastRuns.splice(0, lastRuns.length - 5);
219
+ }
220
+ const isFlaky = new Set(lastRuns).size > 1;
221
+ const logs = (currentResult?.stdout || []).map(s => stripAnsi(s.text || '')).join('');
222
+ return {
223
+ ...spec,
224
+ tests: spec.tests.map(test => ({
225
+ ...test,
226
+ history: lastRuns,
227
+ flaky: isFlaky,
228
+ status: currentStatus,
229
+ duration: currentResult?.duration || 0,
230
+ logs: logs
231
+ }))
232
+ };
233
+ })
234
+ });
235
+ }
236
+ if (suite.suites) {
237
+ suite.suites.forEach(nested => traverseSuites(nested, currentTitle));
238
+ }
239
+ };
240
+ this.getTestSuites(report).forEach(s => traverseSuites(s));
241
+ return finalSuites;
242
+ }
243
+ processCategories(report) {
244
+ const categories = {};
245
+ const allSpecs = this.findAllSpecs(this.getTestSuites(report));
246
+ allSpecs.forEach(spec => {
247
+ const result = spec.tests?.[0]?.results?.[0];
248
+ if (result?.status === 'failed' && result.error) {
249
+ // Use the raw error message to preserve newlines and ANSI for stripping/display
250
+ const cleanMessage = stripAnsi(result.error.message || result.error.stack || 'Unknown error');
251
+ let errorName;
252
+ const expectMatch = cleanMessage.match(/Expected: (.*)\s*Received: (.*)/);
253
+ if (expectMatch) {
254
+ const expected = (expectMatch[1] || '').trim();
255
+ const received = (expectMatch[2] || '').trim();
256
+ errorName = `AssertionError: Expected '${expected}' but received '${received}'`;
257
+ }
258
+ else {
259
+ errorName = (cleanMessage.split('\n')[0] || 'Unknown Error').replace('Error: ', '').trim();
260
+ }
261
+ if (!categories[errorName]) {
262
+ categories[errorName] = {
263
+ name: errorName,
264
+ count: 0,
265
+ tests: []
266
+ };
267
+ }
268
+ categories[errorName].count++;
269
+ categories[errorName].tests.push({
270
+ title: `${spec.file} -> ${spec.title}`,
271
+ trace: cleanMessage
272
+ });
273
+ }
274
+ });
275
+ return Object.values(categories);
276
+ }
277
+ processHistoryData(history, currentReport) {
278
+ const allRuns = [...history];
279
+ const today = new Date().toLocaleDateString();
280
+ // Add current report to historical runs for chart, checking for duplicates
281
+ if (!history.find(h => h.report.stats?.startTime === currentReport.stats?.startTime)) {
282
+ allRuns.push({
283
+ date: today,
284
+ report: currentReport,
285
+ });
286
+ }
287
+ return allRuns.map(run => {
288
+ const stats = run.report.stats || {};
289
+ const { expected = 0, unexpected = 0, skipped = 0 } = stats;
290
+ const total = expected + unexpected + skipped;
291
+ return {
292
+ date: run.date,
293
+ passed: expected,
294
+ failed: unexpected,
295
+ skipped,
296
+ passRate: total > 0 ? (expected / total) * 100 : 0
297
+ };
298
+ });
299
+ }
300
+ processTimeline(report) {
301
+ const timeline = [];
302
+ let cumulativeDuration = 0;
303
+ const allSpecs = this.findAllSpecs(this.getTestSuites(report));
304
+ allSpecs.forEach(spec => {
305
+ const result = spec.tests?.[0]?.results?.[0];
306
+ const duration = result?.duration || 0;
307
+ timeline.push({
308
+ name: spec.title,
309
+ start: cumulativeDuration,
310
+ duration: duration,
311
+ status: result?.status || 'skipped'
312
+ });
313
+ cumulativeDuration += duration;
314
+ });
315
+ return timeline;
316
+ }
317
+ generateReportHtml(report) {
318
+ // 1. Stringify the report object
319
+ const reportJson = JSON.stringify(report);
320
+ // 2. Escape the stringified JSON for safe HTML embedding (CRITICAL FIX)
321
+ const safeReportJson = escapeJsonForHtml(reportJson);
322
+ return `
323
+ <!doctype html>
324
+ <html>
325
+ <head>
326
+ <meta charset="utf-8"/>
327
+ <title>DIS API Test Summary - Enhanced</title>
328
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
329
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
330
+ <style>
331
+ :root{
332
+ --bg:#ffffff; --card:#ffffff; --text:#172B4D; --muted:#6B7280; --accent:#2563EB;
333
+ --pass-bg:#e6ffed; --pass-text:#0a7a0a; --fail-bg:#fff0f0; --fail-text:#b00020;
334
+ --skipped-bg:#f0f0f0; --skipped-text:#6B7280; --shadow: 0 6px 18px rgba(15,23,42,0.08);
335
+ --table-header-bg: rgba(15,23,42,0.03); --table-border: rgba(15,23,42,0.04);
336
+ --log-bg: #eef0f3;
337
+ }
338
+ [data-theme="dark"]{
339
+ --bg:#1a202c; --card:#2d3748; --text:#e2e8f0; --muted:#a0aec0; --accent:#63b3ed;
340
+ --pass-bg:#2A4838; --pass-text:#68D391; --fail-bg:#582C2C; --fail-text:#FC8181;
341
+ --skipped-bg:#4a5568; --skipped-text:#e2e8f0; --shadow: 0 6px 18px rgba(0,0,0,0.4);
342
+ --table-header-bg: rgba(255,255,255,0.05); --table-border: rgba(255,255,255,0.08);
343
+ --log-bg: #1e293b;
344
+ }
345
+ body {
346
+ margin: 0; padding: 0; font-family: 'Inter', sans-serif; font-size: 14px;
347
+ line-height: 1.6; color: var(--text); background-color: var(--bg); transition: background-color 0.3s, color 0.3s;
348
+ }
349
+ .app-container { display: flex; min-height: 100vh; }
350
+ .sidebar { width: 250px; background-color: var(--card); padding: 24px; box-shadow: var(--shadow); z-index: 10; flex-shrink: 0; }
351
+ .sidebar-header { font-weight: 700; font-size: 1.5em; margin-bottom: 30px; color: var(--accent); }
352
+ .sidebar-menu { list-style: none; padding: 0; }
353
+ .sidebar-menu-item { margin-bottom: 12px; }
354
+ .sidebar-menu-item a { display: block; padding: 12px 16px; color: var(--text); text-decoration: none; border-radius: 8px; transition: background-color 0.2s, color 0.2s; }
355
+ .sidebar-menu-item a:hover { background-color: var(--table-header-bg); }
356
+ .sidebar-menu-item a.active { background-color: var(--accent); color: white; }
357
+ .main-content { flex-grow: 1; padding: 32px; overflow-x: auto; }
358
+ .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 16px;}
359
+ .header h1 { font-size: 2em; font-weight: 600; margin: 0; }
360
+ .filters, .actions { display: flex; gap: 16px; align-items: center; }
361
+ .filters label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
362
+ .card { background-color: var(--card); border-radius: 12px; padding: 24px; box-shadow: var(--shadow); margin-bottom: 24px; }
363
+ h2 { font-size: 1.5em; font-weight: 600; margin: 0 0 20px; display: flex; justify-content: space-between; align-items: center;}
364
+ h3 { font-size: 1.1em; font-weight: 600; margin: 0 0 12px; text-align: center; }
365
+ .header-stats { display: flex; gap: 8px; font-size: 0.7em; font-weight: normal; }
366
+ .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; }
367
+ .kpi-card { text-align: center; padding: 16px; border-radius: 8px; background-color: var(--bg); border: 1px solid var(--table-border); }
368
+ .kpi-value { font-size: 2em; font-weight: 700; color: var(--text); }
369
+ .kpi-label { color: var(--muted); font-size: 0.9em; }
370
+ .kpi-pass-rate .kpi-value { color: var(--pass-text); }
371
+ .kpi-failed .kpi-value { color: var(--fail-text); }
372
+ .status-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-weight: 600; font-size: 0.8em; text-transform: uppercase; }
373
+ .status-passed { background-color: var(--pass-bg); color: var(--pass-text); }
374
+ .status-failed { background-color: var(--fail-bg); color: var(--fail-text); }
375
+ .status-skipped { background-color: var(--skipped-bg); color: var(--skipped-text); }
376
+ table { width: 100%; border-collapse: collapse; }
377
+ th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--table-border); }
378
+ th { background-color: var(--table-header-bg); font-weight: 600; }
379
+ tr:last-child td { border-bottom: none; }
380
+ .suite-header { cursor: pointer; padding: 12px 16px; background-color: var(--table-header-bg); border-bottom: 1px solid var(--table-border); display: flex; align-items: center; gap: 8px; font-weight: 600; }
381
+ .collapse-icon { width: 1em; height: 1em; text-align: center; transition: transform 0.2s; }
382
+ .collapse-icon.expanded { transform: rotate(90deg); }
383
+ .test-card-header { cursor: pointer; display: grid; grid-template-columns: 1fr auto auto auto; gap: 16px; align-items: center; padding: 8px 12px;}
384
+ .test-card-header:hover { background-color: var(--table-header-bg); }
385
+ .test-card { border-bottom: 1px solid var(--table-border); }
386
+ .log-row { background-color: var(--table-header-bg); }
387
+ .log-container { padding: 16px; }
388
+ .log-container h4 { margin: 0 0 8px; text-align: left;}
389
+ .log-container pre { white-space: pre-wrap; word-wrap: break-word; background: var(--log-bg); padding: 12px; border-radius: 8px; max-height: 300px; overflow-y: auto; color: var(--muted); font-family: monospace; }
390
+ .dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 4px; }
391
+ .dot.passed { background-color: #4CAF50; }
392
+ .dot.failed { background-color: #F44336; }
393
+ .dot.skipped { background-color: #9E9E9E; }
394
+ .flaky-badge { background-color: #ff9900; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.7em; font-weight: bold; }
395
+ .dark-mode-toggle { cursor: pointer; font-size: 24px; padding: 8px; border-radius: 50%; transition: background-color 0.2s; line-height: 1; }
396
+ .dark-mode-toggle:hover { background-color: var(--table-header-bg); }
397
+ .tab-content { display: none; }
398
+ .tab-content.active { display: block; }
399
+ .download-buttons { display: flex; gap: 10px; }
400
+ .download-buttons button { padding: 10px 16px; border: 1px solid var(--table-border); background-color: var(--card); color: var(--text); border-radius: 8px; cursor: pointer; transition: background-color 0.2s; }
401
+ .download-buttons button:hover { background-color: var(--table-header-bg); }
402
+ .chart-container { position: relative; display: flex; align-items: center; justify-content: center; color: var(--muted); }
403
+ .category-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; padding: 12px; border-bottom: 1px solid var(--table-border); }
404
+ .category-content { padding: 0 12px; }
405
+ .category-test { border-bottom: 1px solid var(--table-border); padding: 12px 0; }
406
+ .category-test:last-child { border-bottom: none; }
407
+ .category-test pre { background-color: var(--bg); padding: 8px; border-radius: 6px; margin-top: 8px; font-family: monospace; }
408
+ .metadata-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 20px;}
409
+ .metadata-item { background-color: var(--bg); padding: 12px; border-radius: 8px; border: 1px solid var(--table-border); }
410
+ .metadata-label { font-weight: 600; color: var(--muted); display: block; margin-bottom: 4px;}
411
+ .metadata-value { font-size: 1.1em;}
412
+ </style>
413
+ </head>
414
+ <body>
415
+
416
+ <div class="app-container">
417
+ <div class="sidebar">
418
+ <div class="sidebar-header">Test Report</div>
419
+ <ul class="sidebar-menu">
420
+ <li class="sidebar-menu-item"><a href="#" class="active" data-tab="overview">Overview</a></li>
421
+ <li class="sidebar-menu-item"><a href="#" data-tab="categories">Categories</a></li>
422
+ <li class="sidebar-menu-item"><a href="#" data-tab="suites">Suites</a></li>
423
+ <li class="sidebar-menu-item"><a href="#" data-tab="graphs">Graphs</a></li>
424
+ <li class="sidebar-menu-item"><a href="#" data-tab="timeline">Timeline</a></li>
425
+ <li class="sidebar-menu-item"><a href="#" data-tab="history">Historical Runs</a></li>
426
+ </ul>
427
+ </div>
428
+
429
+ <div class="main-content">
430
+ <div class="header">
431
+ <h1>Test Execution Report</h1>
432
+ <div class="download-buttons">
433
+ <button id="downloadHtml">Download HTML</button>
434
+ <button id="downloadCsv">Download CSV</button>
435
+ <button id="downloadJson">Download JSON</button>
436
+ <button id="downloadPdf">Download PDF</button>
437
+ </div>
438
+ <div class="actions">
439
+ <div class="filters">
440
+ <label><input type="checkbox" id="filterPassed" checked> Passed</label>
441
+ <label><input type="checkbox" id="filterFailed" checked> Failed</label>
442
+ <label><input type="checkbox" id="filterSkipped" checked> Skipped</label>
443
+ </div>
444
+ <div class="dark-mode-toggle" id="darkModeToggle">☀️</div>
445
+ </div>
446
+ </div>
447
+
448
+ <div id="overview" class="tab-content active">
449
+ <div class="card">
450
+ <h2>Key Performance Indicators</h2>
451
+ <div class="kpi-grid" id="kpiGrid"></div>
452
+ </div>
453
+ <div class="card">
454
+ <h2>Execution Metadata</h2>
455
+ <div class="metadata-grid" id="metadataGrid"></div>
456
+ </div>
457
+ </div>
458
+
459
+ <div id="categories" class="tab-content">
460
+ <div class="card">
461
+ <h2>Failed Test Categories</h2>
462
+ <div id="categoryList"></div>
463
+ </div>
464
+ </div>
465
+
466
+ <div id="suites" class="tab-content">
467
+ <div class="card">
468
+ <h2 id="suitesHeader">Suites & Tests</h2>
469
+ <div id="suitesContainer"></div>
470
+ </div>
471
+ </div>
472
+
473
+ <div id="graphs" class="tab-content">
474
+ <div class="card">
475
+ <h2>Test Graphs & Trends</h2>
476
+ <div style="display: flex; flex-wrap: wrap; gap: 24px; align-items: flex-start;">
477
+ <div style="flex: 1 1 300px; min-width: 300px;">
478
+ <h3>Pass/Fail/Skip</h3>
479
+ <div class="chart-container" style="height: 300px;">
480
+ <canvas id="statusChart"></canvas>
481
+ <div>Loading Chart...</div>
482
+ </div>
483
+ </div>
484
+ <div style="flex: 1 1 400px; min-width: 400px;">
485
+ <h3>Duration Distribution</h3>
486
+ <div class="chart-container" style="height: 300px;">
487
+ <canvas id="durationHistogram"></canvas>
488
+ <div>Loading Chart...</div>
489
+ </div>
490
+ </div>
491
+ <div style="flex: 1 1 100%; min-width: 400px;">
492
+ <h3>Pass Rate Trend</h3>
493
+ <div class="chart-container" style="height: 300px;">
494
+ <canvas id="historyChart"></canvas>
495
+ <div>Loading Chart...</div>
496
+ </div>
497
+ </div>
498
+ </div>
499
+ </div>
500
+ </div>
501
+
502
+ <div id="timeline" class="tab-content">
503
+ <div class="card">
504
+ <h2>Test Execution Timeline</h2>
505
+ <div id="timelineContainer" class="chart-container" style="height: 600px;">
506
+ <canvas id="timelineChart"></canvas>
507
+ <div>Loading Chart...</div>
508
+ </div>
509
+ </div>
510
+ </div>
511
+
512
+ <div id="history" class="tab-content">
513
+ <div class="card">
514
+ <h2 id="historyRunsTitle">Historical Runs</h2>
515
+ <div id="historyGraphContainer" class="chart-container" style="height: 400px;">
516
+ <canvas id="historicalRunsChart"></canvas>
517
+ <div>Loading Chart...</div>
518
+ </div>
519
+ </div>
520
+ </div>
521
+
522
+ </div>
523
+ </div>
524
+
525
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
526
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
527
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
528
+
529
+ <script>
530
+ const reportData = ${safeReportJson};
531
+
532
+ (function() {
533
+ const root = document.documentElement;
534
+ const darkModeToggle = document.getElementById('darkModeToggle');
535
+ const renderedTabs = new Set();
536
+
537
+ function applyTheme(theme) {
538
+ if (theme === 'dark') {
539
+ root.setAttribute('data-theme', 'dark');
540
+ darkModeToggle.textContent = '🌙';
541
+ } else {
542
+ root.setAttribute('data-theme', 'light');
543
+ darkModeToggle.textContent = '☀️';
544
+ }
545
+ }
546
+
547
+ function toggleTheme() {
548
+ const currentTheme = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
549
+ localStorage.setItem('theme', currentTheme);
550
+ applyTheme(currentTheme);
551
+ }
552
+
553
+ darkModeToggle.addEventListener('click', toggleTheme);
554
+
555
+ const savedTheme = localStorage.getItem('theme');
556
+ if (savedTheme) {
557
+ applyTheme(savedTheme);
558
+ }
559
+
560
+ const tabs = document.querySelectorAll('.sidebar-menu-item a');
561
+ const tabContents = document.querySelectorAll('.tab-content');
562
+
563
+ tabs.forEach(tab => {
564
+ tab.addEventListener('click', (e) => {
565
+ e.preventDefault();
566
+ const targetId = e.target.getAttribute('data-tab');
567
+ tabs.forEach(t => t.classList.remove('active'));
568
+ e.target.classList.add('active');
569
+ tabContents.forEach(c => c.classList.remove('active'));
570
+ document.getElementById(targetId).classList.add('active');
571
+ if (renderedTabs.has(targetId)) return;
572
+ if (targetId === 'graphs') { renderCharts(); renderedTabs.add(targetId); }
573
+ if (targetId === 'timeline') { renderTimeline(); renderedTabs.add(targetId); }
574
+ if (targetId === 'history') { renderHistoryGraph(); renderedTabs.add(targetId); }
575
+ });
576
+ });
577
+
578
+ function renderOverview() {
579
+ const { stats, metadata } = reportData;
580
+ const kpiGrid = document.getElementById('kpiGrid');
581
+ kpiGrid.innerHTML = \`
582
+ <div class="kpi-card kpi-total"><div class="kpi-value">\${stats.total}</div><div class="kpi-label">Total Tests</div></div>
583
+ <div class="kpi-card kpi-passed"><div class="kpi-value">\${stats.passed}</div><div class="kpi-label">Passed</div></div>
584
+ <div class="kpi-card kpi-failed"><div class="kpi-value">\${stats.failed}</div><div class="kpi-label">Failed</div></div>
585
+ <div class="kpi-card kpi-skipped"><div class="kpi-value">\${stats.skipped}</div><div class="kpi-label">Skipped</div></div>
586
+ <div class="kpi-card kpi-pass-rate"><div class="kpi-value">\${stats.passRate}</div><div class="kpi-label">Pass Rate</div></div>
587
+ <div class="kpi-card kpi-duration"><div class="kpi-value">\${stats.totalDuration}</div><div class="kpi-label">Total Duration</div></div>
588
+ \`;
589
+
590
+ const metadataGrid = document.getElementById('metadataGrid');
591
+ metadataGrid.innerHTML = \`
592
+ <div class="metadata-item"><span class="metadata-label">Start Time</span><span class="metadata-value">\${metadata.startTime}</span></div>
593
+ <div class="metadata-item"><span class="metadata-label">Total Duration</span><span class="metadata-value">\${metadata.totalDuration}</span></div>
594
+ <div class="metadata-item"><span class="metadata-label">Browser</span><span class="metadata-value">\${metadata.browser}</span></div>
595
+ <div class="metadata-item"><span class="metadata-label">Operating System</span><span class="metadata-value">\${metadata.os}</span></div>
596
+ \`;
597
+ }
598
+
599
+ function renderCategories() {
600
+ const { categories } = reportData;
601
+ const categoryList = document.getElementById('categoryList');
602
+ if (!categories || categories.length === 0) {
603
+ categoryList.innerHTML = '<p>No failed tests categorized.</p>';
604
+ return;
605
+ }
606
+ categoryList.innerHTML = categories.map(cat => \`
607
+ <div class="card">
608
+ <div class="category-header">
609
+ <span>\${cat.name}</span>
610
+ <span class="status-badge status-failed">\${cat.count} failed</span>
611
+ </div>
612
+ <div class="category-content" style="display: none;">
613
+ \${cat.tests.map(test => \`
614
+ <div class="category-test">
615
+ <strong>\${test.title}</strong>
616
+ <pre>\${test.trace}</pre>
617
+ </div>
618
+ \`).join('')}
619
+ </div>
620
+ </div>
621
+ \`).join('');
622
+
623
+ document.querySelectorAll('.category-header').forEach(header => {
624
+ header.addEventListener('click', () => {
625
+ const content = header.nextElementSibling;
626
+ content.style.display = content.style.display === 'none' ? 'block' : 'none';
627
+ });
628
+ });
629
+ }
630
+
631
+ function renderSuites() {
632
+ const { suites, stats } = reportData;
633
+ const suitesContainer = document.getElementById('suitesContainer');
634
+ const suitesHeader = document.getElementById('suitesHeader');
635
+
636
+ suitesHeader.innerHTML = \`
637
+ Suites & Tests
638
+ <div class="header-stats">
639
+ <span class="status-badge status-passed">\${stats.passed} Passed</span>
640
+ <span class="status-badge status-failed">\${stats.failed} Failed</span>
641
+ <span class="status-badge status-skipped">\${stats.skipped} Skipped</span>
642
+ </div>
643
+ \`;
644
+
645
+ suitesContainer.innerHTML = suites.map(suite => \`
646
+ <div class="card">
647
+ <div class="suite-header">
648
+ <svg class="collapse-icon" fill="currentColor" viewBox="0 0 20 20" style="width:20px; height:20px;"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
649
+ \${suite.title}
650
+ </div>
651
+ <div class="suite-content" style="display: none;">
652
+ \${suite.specs.flatMap(spec =>
653
+ spec.tests.map(test => \`
654
+ <div class="test-card">
655
+ <div class="test-card-header">
656
+ <span style="font-weight: 500;">\${spec.title}</span>
657
+ <span class="status-badge status-\${test.status}">\${test.status}</span>
658
+ <span>\${(test.duration / 1000).toFixed(2)}s</span>
659
+ <span>\${test.history.map(h => \`<span class="dot \${h}"></span>\`).join('')}</span>
660
+ </div>
661
+ \${test.logs ? \`
662
+ <div class="log-row" style="display: none;">
663
+ <div class="log-container">
664
+ <h4>Logs</h4>
665
+ <pre>\${test.logs.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</pre>
666
+ </div>
667
+ </div>\` : ''}
668
+ </div>
669
+ \`)
670
+ ).join('')}
671
+ </div>
672
+ </div>
673
+ \`).join('');
674
+
675
+ document.querySelectorAll('.suite-header').forEach(header => {
676
+ header.addEventListener('click', () => {
677
+ const content = header.nextElementSibling;
678
+ const icon = header.querySelector('.collapse-icon');
679
+ const isExpanded = content.style.display === 'block';
680
+ content.style.display = isExpanded ? 'none' : 'block';
681
+ icon.classList.toggle('expanded', !isExpanded);
682
+ });
683
+ });
684
+
685
+ document.querySelectorAll('.test-card-header').forEach(row => {
686
+ row.addEventListener('click', (e) => {
687
+ const logRow = row.nextElementSibling;
688
+ if (logRow && logRow.classList.contains('log-row')) {
689
+ logRow.style.display = logRow.style.display === 'none' ? 'block' : 'none';
690
+ }
691
+ });
692
+ });
693
+ }
694
+
695
+ let statusChart, durationChart, historyChart, timelineChart, historicalRunsChart;
696
+
697
+ function renderCharts() {
698
+ const statusCtx = document.getElementById('statusChart');
699
+ statusCtx.nextElementSibling.style.display = 'none';
700
+ statusChart = new Chart(statusCtx, {
701
+ type: 'doughnut',
702
+ data: {
703
+ labels: ['Passed', 'Failed', 'Skipped'],
704
+ datasets: [{
705
+ data: [reportData.stats.passed, reportData.stats.failed, reportData.stats.skipped],
706
+ backgroundColor: ['#4CAF50', '#F44336', '#9E9E9E'],
707
+ }]
708
+ },
709
+ options: { responsive: true, maintainAspectRatio: false, animation: { animateScale: true } }
710
+ });
711
+
712
+ // Duration Histogram
713
+ const histogramCtx = document.getElementById('durationHistogram');
714
+ histogramCtx.nextElementSibling.style.display = 'none';
715
+ const durations = reportData.suites.flatMap(s => s.specs.flatMap(sp => sp.tests.map(t => t.duration / 1000)));
716
+ const labels = ['<1s', '1-2s', '2-5s', '5-10s', '10-30s', '30-60s', '>60s'];
717
+ const data = new Array(labels.length).fill(0);
718
+
719
+ durations.forEach(d => {
720
+ if (d < 1) data[0]++;
721
+ else if (d < 2) data[1]++;
722
+ else if (d < 5) data[2]++;
723
+ else if (d < 10) data[3]++;
724
+ else if (d < 30) data[4]++;
725
+ else if (d < 60) data[5]++;
726
+ else data[6]++;
727
+ });
728
+
729
+ new Chart(histogramCtx, {
730
+ type: 'bar',
731
+ data: {
732
+ labels: labels,
733
+ datasets: [{
734
+ label: 'Number of Tests',
735
+ data: data,
736
+ backgroundColor: 'rgba(153, 102, 255, 0.6)',
737
+ }]
738
+ },
739
+ options: {
740
+ responsive: true,
741
+ maintainAspectRatio: false,
742
+ scales: { y: { beginAtZero: true, title: { display: true, text: 'Test Count' } } }
743
+ }
744
+ });
745
+
746
+
747
+ const historyCtx = document.getElementById('historyChart');
748
+ historyCtx.nextElementSibling.style.display = 'none';
749
+ historyChart = new Chart(historyCtx, {
750
+ type: 'line',
751
+ data: {
752
+ labels: reportData.history.map(h => h.date),
753
+ datasets: [{
754
+ label: 'Pass Rate (%)',
755
+ data: reportData.history.map(h => h.passRate),
756
+ borderColor: 'rgba(75, 192, 192, 1)',
757
+ fill: false,
758
+ tension: 0.1
759
+ }]
760
+ },
761
+ options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: 100 } } }
762
+ });
763
+ }
764
+
765
+ function renderTimeline() {
766
+ const timelineCtx = document.getElementById('timelineChart');
767
+ timelineCtx.nextElementSibling.style.display = 'none';
768
+
769
+ // Transform timeline data for Chart.js gantt-like chart
770
+ const labels = reportData.timeline.map(t => t.name);
771
+ const datasets = [{
772
+ label: 'Execution Time',
773
+ data: reportData.timeline.map(t => [t.start, t.start + t.duration]),
774
+ backgroundColor: reportData.timeline.map(t => {
775
+ if (t.status === 'passed') return 'rgba(76, 175, 80, 0.8)';
776
+ if (t.status === 'failed') return 'rgba(244, 67, 54, 0.8)';
777
+ return 'rgba(158, 158, 158, 0.8)';
778
+ }),
779
+ }];
780
+
781
+ // Only render chart if there is data
782
+ if (reportData.timeline.length > 0) {
783
+ timelineCtx.parentElement.style.height = (reportData.timeline.length * 30 + 100) + 'px'; // Dynamic height based on number of tests
784
+ timelineChart = new Chart(timelineCtx, {
785
+ type: 'bar',
786
+ data: {
787
+ labels: labels,
788
+ datasets: datasets
789
+ },
790
+ options: {
791
+ indexAxis: 'y',
792
+ responsive: true,
793
+ maintainAspectRatio: false,
794
+ plugins: { legend: { display: false } },
795
+ scales: {
796
+ x: { min: 0, title: { display: true, text: 'Time (ms)' } },
797
+ }
798
+ }
799
+ });
800
+ } else {
801
+ timelineCtx.parentElement.innerHTML = '<div>No timeline data available.</div>';
802
+ }
803
+ }
804
+
805
+ function renderHistoryGraph() {
806
+ const history = reportData.history;
807
+ document.getElementById('historyRunsTitle').textContent = \`Historical Runs (Count: \${history.length})\`;
808
+ const historyCtx = document.getElementById('historicalRunsChart');
809
+ historyCtx.nextElementSibling.style.display = 'none';
810
+
811
+ // Only render chart if there is data
812
+ if (history.length > 0) {
813
+ historicalRunsChart = new Chart(historyCtx, {
814
+ type: 'bar',
815
+ data: {
816
+ labels: history.map(h => h.date),
817
+ datasets: [
818
+ {
819
+ type: 'line',
820
+ label: 'Pass Rate',
821
+ data: history.map(h => h.passRate),
822
+ borderColor: 'rgb(255, 159, 64)',
823
+ yAxisID: 'y1',
824
+ tension: 0.1,
825
+ pointRadius: 5,
826
+ pointBackgroundColor: 'rgb(255, 159, 64)',
827
+ order: 0, // Draw line on top of bars
828
+ },
829
+ {
830
+ type: 'bar',
831
+ label: 'Passed',
832
+ data: history.map(h => h.passed),
833
+ backgroundColor: 'rgba(75, 192, 192, 0.8)',
834
+ order: 1,
835
+ },
836
+ {
837
+ type: 'bar',
838
+ label: 'Failed',
839
+ data: history.map(h => h.failed),
840
+ backgroundColor: 'rgba(255, 99, 132, 0.8)',
841
+ order: 1,
842
+ },
843
+ {
844
+ type: 'bar',
845
+ label: 'Skipped',
846
+ data: history.map(h => h.skipped),
847
+ backgroundColor: 'rgba(158, 158, 158, 0.8)',
848
+ order: 1,
849
+ }
850
+ ]
851
+ },
852
+ options: {
853
+ responsive: true,
854
+ maintainAspectRatio: false,
855
+ scales: {
856
+ x: { stacked: true, },
857
+ y: {
858
+ stacked: true,
859
+ beginAtZero: true,
860
+ title: { display: true, text: 'Test Count' }
861
+ },
862
+ y1: {
863
+ position: 'right',
864
+ beginAtZero: true,
865
+ max: 100,
866
+ title: { display: true, text: 'Pass Rate (%)' },
867
+ grid: { drawOnChartArea: false }
868
+ }
869
+ },
870
+ tooltips: {
871
+ mode: 'index',
872
+ intersect: false
873
+ }
874
+ }
875
+ });
876
+ } else {
877
+ historyCtx.parentElement.innerHTML = '<div>No historical runs data available.</div>';
878
+ }
879
+ }
880
+
881
+ const filterPassed = document.getElementById('filterPassed');
882
+ const filterFailed = document.getElementById('filterFailed');
883
+ const filterSkipped = document.getElementById('filterSkipped');
884
+
885
+ function applyFilters() {
886
+ const showPassed = filterPassed.checked;
887
+ const showFailed = filterFailed.checked;
888
+ const showSkipped = filterSkipped.checked;
889
+
890
+ // Hide all log rows initially when filters change
891
+ document.querySelectorAll('.log-row').forEach(logRow => {
892
+ logRow.style.display = 'none';
893
+ });
894
+
895
+ document.querySelectorAll('.test-card').forEach(card => {
896
+ const header = card.querySelector('.test-card-header');
897
+ const status = header.querySelector('.status-badge').textContent.toLowerCase();
898
+ let display = false;
899
+ if (status === 'passed' && showPassed) display = true;
900
+ if (status === 'failed' && showFailed) display = true;
901
+ if (status === 'skipped' && showSkipped) display = true;
902
+ card.style.display = display ? '' : 'none';
903
+
904
+ // Hide the parent suite card if all its tests are hidden
905
+ let parentSuiteContent = card.closest('.suite-content');
906
+ if (parentSuiteContent) {
907
+ let visibleTests = Array.from(parentSuiteContent.querySelectorAll('.test-card')).filter(t => t.style.display !== 'none');
908
+ let parentCard = parentSuiteContent.closest('.card');
909
+ if (parentCard) {
910
+ parentCard.style.display = visibleTests.length > 0 ? '' : 'none';
911
+ }
912
+ }
913
+ });
914
+ }
915
+ filterPassed.addEventListener('change', applyFilters);
916
+ filterFailed.addEventListener('change', applyFilters);
917
+ filterSkipped.addEventListener('change', applyFilters);
918
+
919
+ document.getElementById('downloadHtml').addEventListener('click', () => {
920
+ const htmlContent = '<!DOCTYPE html>' + document.documentElement.outerHTML;
921
+ const blob = new Blob([htmlContent], { type: 'text/html' });
922
+ const a = document.createElement('a');
923
+ a.href = URL.createObjectURL(blob);
924
+ a.download = 'report.html';
925
+ a.click();
926
+ URL.revokeObjectURL(a.href);
927
+ });
928
+ document.getElementById('downloadJson').addEventListener('click', () => {
929
+ const jsonContent = JSON.stringify(reportData, null, 2);
930
+ const blob = new Blob([jsonContent], { type: 'application/json' });
931
+ const a = document.createElement('a');
932
+ a.href = URL.createObjectURL(blob);
933
+ a.download = 'report-data.json';
934
+ a.click();
935
+ URL.revokeObjectURL(a.href);
936
+ });
937
+ document.getElementById('downloadCsv').addEventListener('click', () => {
938
+ let csvContent = "data:text/csv;charset=utf-8,Suite,Test,Status,Duration (s),History\\n";
939
+ reportData.suites.forEach(suite => {
940
+ suite.specs.forEach(spec => {
941
+ const testData = spec.tests && spec.tests[0];
942
+ const status = testData ? testData.status : 'N/A';
943
+ const duration = testData ? (testData.duration / 1000).toFixed(2) : 'N/A';
944
+ const history = testData && testData.history ? testData.history.join(" | ") : 'N/A';
945
+
946
+ const row = [\`"\${suite.title.replace(/"/g,'""')}"\`,\`"\${spec.title.replace(/"/g,'""')}"\`,status,duration,history].join(",");
947
+ csvContent += row + '\\n';
948
+ });
949
+ });
950
+ const encodedUri = encodeURI(csvContent);
951
+ const a = document.createElement('a'); a.href = encodedUri; a.download = 'test_report.csv'; a.click();
952
+ });
953
+ document.getElementById('downloadPdf').addEventListener('click', () => {
954
+ const doc = new window.jspdf.jsPDF();
955
+ const element = document.querySelector('.main-content');
956
+ html2canvas(element, { scale: 2 }).then(canvas => {
957
+ const imgData = canvas.toDataURL('image/png');
958
+ const imgWidth = 210; const pageHeight = 295;
959
+ const imgHeight = canvas.height * imgWidth / canvas.width;
960
+ let heightLeft = imgHeight; let position = 0;
961
+ doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
962
+ heightLeft -= pageHeight;
963
+ while (heightLeft >= 0) {
964
+ position = heightLeft - imgHeight;
965
+ doc.addPage();
966
+ doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
967
+ }
968
+ doc.save('report.pdf');
969
+ });
970
+ });
971
+
972
+ renderOverview();
973
+ renderCategories();
974
+ renderSuites();
975
+ applyFilters();
976
+
977
+ })();
978
+ </script>
979
+ </body>
980
+ </html>
981
+ `;
982
+ }
983
+ }
984
+ module.exports = AggregatorReporter;
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "slik-report",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A powerful Playwright reporter for aggregating test results, flakiness analysis, and historical trend tracking.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "npx tsc",
9
+ "prebuild": "npm run test",
10
+ "test": "jest",
9
11
  "prepublishOnly": "npm run build"
10
12
  },
11
13
  "keywords": [
@@ -18,13 +20,20 @@
18
20
  "author": "Your Name Here",
19
21
  "license": "ISC",
20
22
  "files": [
21
- "dist"
23
+ "dist/index.js",
24
+ "dist/index.d.ts",
25
+ "README.md"
22
26
  ],
23
27
  "devDependencies": {
24
- "@types/node": "^20.11.24",
28
+ "@playwright/test": "^1.57.0",
29
+ "@types/jest": "^30.0.0",
30
+ "@types/node": "^20.19.25",
31
+ "jest": "^30.2.0",
32
+ "slik-report": "^1.0.1",
33
+ "ts-jest": "^29.4.5",
25
34
  "typescript": "^5.3.3"
26
35
  },
27
36
  "peerDependencies": {
28
- "playwright": ">=1.30.0"
37
+ "playwright": "^1.57.0"
29
38
  }
30
39
  }
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}