playwright-network-metrics 0.1.2 → 0.2.1
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/dist/index.d.mts +4 -21
- package/dist/index.d.ts +4 -21
- package/dist/index.js +95 -87
- package/dist/index.mjs +95 -87
- package/package.json +3 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request, Fixtures } from '@playwright/test';
|
|
2
|
-
import { Reporter, TestCase, TestResult } from '@playwright/test/reporter';
|
|
2
|
+
import { Reporter, FullConfig, TestCase, TestResult } from '@playwright/test/reporter';
|
|
3
3
|
|
|
4
4
|
declare const RESOURCE_TYPES: string[];
|
|
5
5
|
|
|
@@ -56,21 +56,6 @@ interface NetworkMetricsConfig {
|
|
|
56
56
|
*/
|
|
57
57
|
routeGroupFn?: (url: string) => string | undefined;
|
|
58
58
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Configuration for the Playwright reporter plugin.
|
|
61
|
-
*/
|
|
62
|
-
interface NetworkMetricsReporterConfig {
|
|
63
|
-
/**
|
|
64
|
-
* Directory where the metrics reports (JSON/HTML) will be saved.
|
|
65
|
-
* @default "playwright-report/network-metrics"
|
|
66
|
-
*/
|
|
67
|
-
outDir?: string;
|
|
68
|
-
/**
|
|
69
|
-
* Whether to generate an interactive HTML report.
|
|
70
|
-
* @default false
|
|
71
|
-
*/
|
|
72
|
-
html?: boolean;
|
|
73
|
-
}
|
|
74
59
|
|
|
75
60
|
/**
|
|
76
61
|
* Returns a Playwright fixture tuple with automatic instrumentation.
|
|
@@ -107,13 +92,11 @@ declare class NetworkMetricsReporter implements Reporter {
|
|
|
107
92
|
* Internal store for all collected request metrics from all tests.
|
|
108
93
|
*/
|
|
109
94
|
private allMetrics;
|
|
110
|
-
private config;
|
|
111
95
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* @param config Configuration for output directory and HTML report generation.
|
|
96
|
+
* Configuration for the plugin.
|
|
115
97
|
*/
|
|
116
|
-
|
|
98
|
+
private config;
|
|
99
|
+
onBegin(config: FullConfig): void;
|
|
117
100
|
/**
|
|
118
101
|
* Playwright lifecycle hook called after each test finishes.
|
|
119
102
|
* Extracts "network-metrics" attachments and stores them for end-of-run aggregation.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request, Fixtures } from '@playwright/test';
|
|
2
|
-
import { Reporter, TestCase, TestResult } from '@playwright/test/reporter';
|
|
2
|
+
import { Reporter, FullConfig, TestCase, TestResult } from '@playwright/test/reporter';
|
|
3
3
|
|
|
4
4
|
declare const RESOURCE_TYPES: string[];
|
|
5
5
|
|
|
@@ -56,21 +56,6 @@ interface NetworkMetricsConfig {
|
|
|
56
56
|
*/
|
|
57
57
|
routeGroupFn?: (url: string) => string | undefined;
|
|
58
58
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Configuration for the Playwright reporter plugin.
|
|
61
|
-
*/
|
|
62
|
-
interface NetworkMetricsReporterConfig {
|
|
63
|
-
/**
|
|
64
|
-
* Directory where the metrics reports (JSON/HTML) will be saved.
|
|
65
|
-
* @default "playwright-report/network-metrics"
|
|
66
|
-
*/
|
|
67
|
-
outDir?: string;
|
|
68
|
-
/**
|
|
69
|
-
* Whether to generate an interactive HTML report.
|
|
70
|
-
* @default false
|
|
71
|
-
*/
|
|
72
|
-
html?: boolean;
|
|
73
|
-
}
|
|
74
59
|
|
|
75
60
|
/**
|
|
76
61
|
* Returns a Playwright fixture tuple with automatic instrumentation.
|
|
@@ -107,13 +92,11 @@ declare class NetworkMetricsReporter implements Reporter {
|
|
|
107
92
|
* Internal store for all collected request metrics from all tests.
|
|
108
93
|
*/
|
|
109
94
|
private allMetrics;
|
|
110
|
-
private config;
|
|
111
95
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* @param config Configuration for output directory and HTML report generation.
|
|
96
|
+
* Configuration for the plugin.
|
|
115
97
|
*/
|
|
116
|
-
|
|
98
|
+
private config;
|
|
99
|
+
onBegin(config: FullConfig): void;
|
|
117
100
|
/**
|
|
118
101
|
* Playwright lifecycle hook called after each test finishes.
|
|
119
102
|
* Extracts "network-metrics" attachments and stores them for end-of-run aggregation.
|
package/dist/index.js
CHANGED
|
@@ -140,9 +140,9 @@ var NetworkMetricsCollector = class {
|
|
|
140
140
|
const urlWithQuery = this.redactUrl(request.url());
|
|
141
141
|
const urlParts = new URL(urlWithQuery);
|
|
142
142
|
const url = `${urlParts.protocol}//${urlParts.host}${urlParts.pathname}`;
|
|
143
|
-
const duration = timing.
|
|
143
|
+
const duration = timing.requestStart >= 0 && timing.responseStart >= 0 ? timing.responseStart - timing.requestStart : -1;
|
|
144
|
+
const loadTime = timing.responseEnd >= 0 && timing.responseStart >= 0 ? timing.responseEnd - timing.responseStart : -1;
|
|
144
145
|
if (duration < 0) {
|
|
145
|
-
console.warn(`Invalid duration for request ${url}: ${duration}`);
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
const metric = {
|
|
@@ -151,6 +151,7 @@ var NetworkMetricsCollector = class {
|
|
|
151
151
|
method: request.method(),
|
|
152
152
|
status: response?.status() ?? 0,
|
|
153
153
|
duration,
|
|
154
|
+
loadTime,
|
|
154
155
|
resourceType: request.resourceType(),
|
|
155
156
|
failed: !response || !response.ok(),
|
|
156
157
|
errorText,
|
|
@@ -271,7 +272,9 @@ var NetworkMetricsAggregator = class {
|
|
|
271
272
|
method: m.method,
|
|
272
273
|
count: 0,
|
|
273
274
|
totalDurationMs: 0,
|
|
275
|
+
totalLoadTimeMs: 0,
|
|
274
276
|
avgDurationMs: 0,
|
|
277
|
+
avgLoadTimeMs: 0,
|
|
275
278
|
p50: 0,
|
|
276
279
|
p95: 0,
|
|
277
280
|
p99: 0,
|
|
@@ -283,6 +286,9 @@ var NetworkMetricsAggregator = class {
|
|
|
283
286
|
}
|
|
284
287
|
am.count++;
|
|
285
288
|
am.totalDurationMs += m.duration;
|
|
289
|
+
if (m.loadTime >= 0) {
|
|
290
|
+
am.totalLoadTimeMs += m.loadTime;
|
|
291
|
+
}
|
|
286
292
|
if (m.failed) am.errorCount++;
|
|
287
293
|
const samples = this.samplesPerKey.get(key) ?? [];
|
|
288
294
|
samples.push(m.duration);
|
|
@@ -311,6 +317,7 @@ var NetworkMetricsAggregator = class {
|
|
|
311
317
|
*/
|
|
312
318
|
finalizeAggregatedMetric(am) {
|
|
313
319
|
am.avgDurationMs = am.count > 0 ? am.totalDurationMs / am.count : 0;
|
|
320
|
+
am.avgLoadTimeMs = am.count > 0 ? am.totalLoadTimeMs / am.count : 0;
|
|
314
321
|
const samples = this.samplesPerKey.get(am.key) ?? [];
|
|
315
322
|
if (samples.length > 0) {
|
|
316
323
|
samples.sort((a, b) => a - b);
|
|
@@ -352,8 +359,8 @@ function generateHtmlReport(report) {
|
|
|
352
359
|
--text-muted: #7f8c8d;
|
|
353
360
|
--border: #e1e8ed;
|
|
354
361
|
}
|
|
355
|
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: var(--text); margin: 0; padding: 20px; background: var(--bg); }
|
|
356
|
-
.container {
|
|
362
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: var(--text); margin: 0; padding: 20px 80px; background: var(--bg); }
|
|
363
|
+
.container { margin: 0 auto; }
|
|
357
364
|
h1 { color: var(--secondary); margin-bottom: 30px; font-weight: 700; }
|
|
358
365
|
|
|
359
366
|
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
@@ -372,32 +379,24 @@ function generateHtmlReport(report) {
|
|
|
372
379
|
.tab:hover { background: #f8f9fa; }
|
|
373
380
|
.tab.active { background: var(--primary); color: white; border-color: var(--primary); }
|
|
374
381
|
|
|
375
|
-
.
|
|
376
|
-
table {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
.grid-wrapper { background: var(--card-bg); border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); border: 1px solid var(--border); overflow: hidden; }
|
|
383
|
+
.grid-table { overflow-x: auto; }
|
|
384
|
+
.grid-header, .grid-row { display: grid; grid-template-columns: minmax(300px, 4fr) 110px 160px 160px 160px 160px 120px; align-items: center; border-bottom: 1px solid var(--border); }
|
|
385
|
+
.grid-header { background: #f8fafc; font-weight: 700; color: var(--secondary); font-size: 0.85rem; text-transform: uppercase; width: fit-content; min-width: 100%; }
|
|
386
|
+
.grid-cell { padding: 14px 18px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
387
|
+
.grid-header .grid-cell { cursor: pointer; position: relative; display: flex; align-items: center; gap: 4px; }
|
|
388
|
+
.grid-header .grid-cell:hover { background: #f1f5f9; }
|
|
389
|
+
|
|
390
|
+
#grid-body { width: fit-content; min-width: 100%; }
|
|
384
391
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
392
|
+
.grid-row { cursor: pointer; transition: background 0.1s; background: white; }
|
|
393
|
+
.grid-row:hover { background: #fcfdfe; }
|
|
394
|
+
.grid-row.expanded { background: #f8fafc; border-bottom: none; }
|
|
388
395
|
|
|
389
|
-
.
|
|
390
|
-
.
|
|
391
|
-
.
|
|
392
|
-
.PUT { background: #fff3e0; color: #f57c00; }
|
|
393
|
-
.DELETE { background: #ffebee; color: #d32f2f; }
|
|
396
|
+
.details-pane { display: none; background: #f8fafc; border-bottom: 1px solid var(--border); }
|
|
397
|
+
.details-pane.show { display: block; }
|
|
398
|
+
.details-content { padding: 20px; }
|
|
394
399
|
|
|
395
|
-
.error-tag { color: var(--error); font-weight: 700; }
|
|
396
|
-
|
|
397
|
-
/* Details row */
|
|
398
|
-
.details-row { display: none; background: #f8fafc; }
|
|
399
|
-
.details-row.show { display: table-row; }
|
|
400
|
-
.details-content { padding: 20px; border-bottom: 1px solid var(--border); }
|
|
401
400
|
.details-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
|
|
402
401
|
.details-section h4 { margin: 0 0 10px 0; font-size: 0.9rem; color: var(--secondary); border-bottom: 1px solid #dee5ed; padding-bottom: 5px; }
|
|
403
402
|
.details-list { margin: 0; padding: 0; list-style: none; font-size: 0.85rem; }
|
|
@@ -405,14 +404,24 @@ function generateHtmlReport(report) {
|
|
|
405
404
|
.details-list li:last-child { border-bottom: none; }
|
|
406
405
|
.count-badge { background: #edf2f7; padding: 2px 8px; border-radius: 10px; font-weight: 600; color: var(--secondary); }
|
|
407
406
|
|
|
408
|
-
|
|
407
|
+
.endpoint-cell { display: flex; align-items: center; }
|
|
408
|
+
.endpoint-key { overflow: hidden; text-overflow: ellipsis; }
|
|
409
|
+
|
|
410
|
+
.method { font-weight: 800; font-size: 0.7rem; padding: 3px 6px; border-radius: 4px; margin-right: 10px; display: inline-block; min-width: 45px; text-align: center; vertical-align: middle; }
|
|
411
|
+
.GET { background: #e3f2fd; color: #1976d2; }
|
|
412
|
+
.POST { background: #e8f5e9; color: #388e3c; }
|
|
413
|
+
.PUT { background: #fff3e0; color: #f57c00; }
|
|
414
|
+
.DELETE { background: #ffebee; color: #d32f2f; }
|
|
415
|
+
|
|
416
|
+
.error-tag { color: var(--error); font-weight: 700; }
|
|
417
|
+
|
|
409
418
|
.tooltip { position: relative; display: inline-block; margin-left: 4px; cursor: help; color: #cbd5e0; }
|
|
410
419
|
.tooltip:hover { color: var(--primary); }
|
|
411
420
|
.tooltip .tooltiptext { visibility: hidden; width: 220px; background-color: #334155; color: #fff; text-align: center; border-radius: 6px; padding: 8px; position: absolute; z-index: 100; bottom: 125%; left: 50%; margin-left: -110px; opacity: 0; transition: opacity 0.2s; font-size: 0.75rem; text-transform: none; font-weight: 400; line-height: 1.4; pointer-events: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
|
412
421
|
.tooltip:hover .tooltiptext { visibility: visible; opacity: 1; }
|
|
413
422
|
|
|
414
423
|
.chevron { display: inline-block; transition: transform 0.2s; margin-right: 8px; width: 12px; height: 12px; fill: #cbd5e0; }
|
|
415
|
-
|
|
424
|
+
.grid-row.expanded .chevron { transform: rotate(90deg); fill: var(--primary); }
|
|
416
425
|
</style>
|
|
417
426
|
</head>
|
|
418
427
|
<body>
|
|
@@ -449,13 +458,11 @@ function generateHtmlReport(report) {
|
|
|
449
458
|
<div class="tab" data-target="resourceTypes">Resource Types</div>
|
|
450
459
|
</div>
|
|
451
460
|
|
|
452
|
-
<div class="
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
<tbody id="table-body"></tbody>
|
|
458
|
-
</table>
|
|
461
|
+
<div class="grid-wrapper">
|
|
462
|
+
<div class="grid-table" id="metrics-grid">
|
|
463
|
+
<div class="grid-header" id="grid-header"></div>
|
|
464
|
+
<div id="grid-body"></div>
|
|
465
|
+
</div>
|
|
459
466
|
</div>
|
|
460
467
|
</div>
|
|
461
468
|
|
|
@@ -470,11 +477,10 @@ function generateHtmlReport(report) {
|
|
|
470
477
|
const columnMeta = {
|
|
471
478
|
key: { label: 'Endpoint / Key', tooltip: 'The unique identifier for this aggregation group.' },
|
|
472
479
|
count: { label: 'Count', tooltip: 'Total number of requests made.' },
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
totalDurationMs: { label: 'Total Time', tooltip: 'Sum of all response times for this group.' },
|
|
480
|
+
avgLoadTimeMs: { label: 'Avg Load', tooltip: 'Resource Load Time: The time taken to download the resource body (responseEnd - responseStart).' },
|
|
481
|
+
avgDurationMs: { label: 'Avg Duration', tooltip: 'The average response time in milliseconds. This represents the total time from request start to the end of the response body.' },
|
|
482
|
+
totalLoadTimeMs: { label: 'Total Load', tooltip: 'Sum of all load times for this group.' },
|
|
483
|
+
totalDurationMs: { label: 'Total Duration', tooltip: 'Sum of all response times for this group.' },
|
|
478
484
|
errorCount: { label: 'Errors', tooltip: 'Number of failed requests (non-2xx response or network error).' }
|
|
479
485
|
};
|
|
480
486
|
|
|
@@ -518,61 +524,58 @@ function generateHtmlReport(report) {
|
|
|
518
524
|
return (valA - valB) * sortOrder;
|
|
519
525
|
});
|
|
520
526
|
|
|
521
|
-
const header = document.getElementById('
|
|
522
|
-
const body = document.getElementById('
|
|
527
|
+
const header = document.getElementById('grid-header');
|
|
528
|
+
const body = document.getElementById('grid-body');
|
|
523
529
|
|
|
524
|
-
const columns = ['key', 'count', '
|
|
530
|
+
const columns = ['key', 'count', 'avgLoadTimeMs', 'totalLoadTimeMs', 'avgDurationMs', 'totalDurationMs', 'errorCount'];
|
|
525
531
|
|
|
526
532
|
header.innerHTML = columns.map(k => \`
|
|
527
|
-
<
|
|
533
|
+
<div class="grid-cell" onclick="handleSort('\${k}')">
|
|
528
534
|
\${columnMeta[k].label}
|
|
529
|
-
<span class="tooltip"
|
|
535
|
+
<span class="tooltip" title="\${columnMeta[k].tooltip}">\u24D8</span>
|
|
530
536
|
\${sortKey === k ? (sortOrder === 1 ? '\u25B2' : '\u25BC') : ''}
|
|
531
|
-
</
|
|
537
|
+
</div>
|
|
532
538
|
\`).join('');
|
|
533
539
|
|
|
534
540
|
let html = '';
|
|
535
541
|
data.forEach(item => {
|
|
536
542
|
const isExpanded = expandedKeys.has(item.key);
|
|
537
543
|
html += \`
|
|
538
|
-
<
|
|
539
|
-
<
|
|
544
|
+
<div class="grid-row \${isExpanded ? 'expanded' : ''}" onclick="toggleExpand('\${item.key}', event)">
|
|
545
|
+
<div class="grid-cell endpoint-cell">
|
|
540
546
|
<svg class="chevron" viewBox="0 0 20 20"><path d="M7 1L16 10L7 19" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
|
541
547
|
\${item.method ? \`<span class="method \${item.method}">\${item.method}</span>\` : ''}
|
|
542
|
-
\${item.key}
|
|
543
|
-
</
|
|
544
|
-
<
|
|
545
|
-
<
|
|
546
|
-
<
|
|
547
|
-
<
|
|
548
|
-
<
|
|
549
|
-
<
|
|
550
|
-
|
|
551
|
-
</tr>
|
|
548
|
+
<span class="endpoint-key" title="\${item.key}">\${item.method ? item.key.replace(new RegExp(\`^\${item.method}\\\\s+\`), '') : item.key}</span>
|
|
549
|
+
</div>
|
|
550
|
+
<div class="grid-cell">\${item.count}</div>
|
|
551
|
+
<div class="grid-cell">\${formatMs(item.avgLoadTimeMs)}</div>
|
|
552
|
+
<div class="grid-cell">\${formatMs(item.totalLoadTimeMs)}</div>
|
|
553
|
+
<div class="grid-cell">\${formatMs(item.avgDurationMs)}</div>
|
|
554
|
+
<div class="grid-cell">\${formatMs(item.totalDurationMs)}</div>
|
|
555
|
+
<div class="grid-cell \${item.errorCount > 0 ? 'error-tag' : ''}">\${item.errorCount}</div>
|
|
556
|
+
</div>
|
|
552
557
|
\`;
|
|
553
558
|
|
|
554
559
|
if (isExpanded) {
|
|
555
560
|
html += \`
|
|
556
|
-
<
|
|
557
|
-
<
|
|
558
|
-
<div class="details-
|
|
559
|
-
<div class="details-
|
|
560
|
-
<
|
|
561
|
-
|
|
562
|
-
<
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
<
|
|
567
|
-
|
|
568
|
-
<
|
|
569
|
-
|
|
570
|
-
</ul>
|
|
571
|
-
</div>
|
|
561
|
+
<div class="details-pane show">
|
|
562
|
+
<div class="details-content">
|
|
563
|
+
<div class="details-grid">
|
|
564
|
+
<div class="details-section">
|
|
565
|
+
<h4>Contributing Spec Files</h4>
|
|
566
|
+
<ul class="details-list">
|
|
567
|
+
\${item.specs.map(s => \`<li><span>\${s.name}</span> <span class="count-badge">\${s.count}</span></li>\`).join('')}
|
|
568
|
+
</ul>
|
|
569
|
+
</div>
|
|
570
|
+
<div class="details-section">
|
|
571
|
+
<h4>Contributing Tests</h4>
|
|
572
|
+
<ul class="details-list">
|
|
573
|
+
\${item.tests.map(t => \`<li><span>\${t.name}</span> <span class="count-badge">\${t.count}</span></li>\`).join('')}
|
|
574
|
+
</ul>
|
|
572
575
|
</div>
|
|
573
576
|
</div>
|
|
574
|
-
</
|
|
575
|
-
</
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
576
579
|
\`;
|
|
577
580
|
}
|
|
578
581
|
});
|
|
@@ -613,18 +616,23 @@ var NetworkMetricsReporter = class {
|
|
|
613
616
|
* Internal store for all collected request metrics from all tests.
|
|
614
617
|
*/
|
|
615
618
|
allMetrics = [];
|
|
616
|
-
config;
|
|
617
619
|
/**
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
* @param config Configuration for output directory and HTML report generation.
|
|
620
|
+
* Configuration for the plugin.
|
|
621
621
|
*/
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
622
|
+
config = {
|
|
623
|
+
outDir: "playwright-report/network-metrics",
|
|
624
|
+
html: true
|
|
625
|
+
};
|
|
626
|
+
onBegin(config) {
|
|
627
|
+
const reporterEntry = config.reporter.find(
|
|
628
|
+
([name]) => name.includes("playwright-network-metrics")
|
|
629
|
+
);
|
|
630
|
+
if (reporterEntry && typeof reporterEntry[1] === "object") {
|
|
631
|
+
this.config = {
|
|
632
|
+
...this.config,
|
|
633
|
+
...reporterEntry[1]
|
|
634
|
+
};
|
|
635
|
+
}
|
|
628
636
|
}
|
|
629
637
|
/**
|
|
630
638
|
* Playwright lifecycle hook called after each test finishes.
|
package/dist/index.mjs
CHANGED
|
@@ -102,9 +102,9 @@ var NetworkMetricsCollector = class {
|
|
|
102
102
|
const urlWithQuery = this.redactUrl(request.url());
|
|
103
103
|
const urlParts = new URL(urlWithQuery);
|
|
104
104
|
const url = `${urlParts.protocol}//${urlParts.host}${urlParts.pathname}`;
|
|
105
|
-
const duration = timing.
|
|
105
|
+
const duration = timing.requestStart >= 0 && timing.responseStart >= 0 ? timing.responseStart - timing.requestStart : -1;
|
|
106
|
+
const loadTime = timing.responseEnd >= 0 && timing.responseStart >= 0 ? timing.responseEnd - timing.responseStart : -1;
|
|
106
107
|
if (duration < 0) {
|
|
107
|
-
console.warn(`Invalid duration for request ${url}: ${duration}`);
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
const metric = {
|
|
@@ -113,6 +113,7 @@ var NetworkMetricsCollector = class {
|
|
|
113
113
|
method: request.method(),
|
|
114
114
|
status: response?.status() ?? 0,
|
|
115
115
|
duration,
|
|
116
|
+
loadTime,
|
|
116
117
|
resourceType: request.resourceType(),
|
|
117
118
|
failed: !response || !response.ok(),
|
|
118
119
|
errorText,
|
|
@@ -233,7 +234,9 @@ var NetworkMetricsAggregator = class {
|
|
|
233
234
|
method: m.method,
|
|
234
235
|
count: 0,
|
|
235
236
|
totalDurationMs: 0,
|
|
237
|
+
totalLoadTimeMs: 0,
|
|
236
238
|
avgDurationMs: 0,
|
|
239
|
+
avgLoadTimeMs: 0,
|
|
237
240
|
p50: 0,
|
|
238
241
|
p95: 0,
|
|
239
242
|
p99: 0,
|
|
@@ -245,6 +248,9 @@ var NetworkMetricsAggregator = class {
|
|
|
245
248
|
}
|
|
246
249
|
am.count++;
|
|
247
250
|
am.totalDurationMs += m.duration;
|
|
251
|
+
if (m.loadTime >= 0) {
|
|
252
|
+
am.totalLoadTimeMs += m.loadTime;
|
|
253
|
+
}
|
|
248
254
|
if (m.failed) am.errorCount++;
|
|
249
255
|
const samples = this.samplesPerKey.get(key) ?? [];
|
|
250
256
|
samples.push(m.duration);
|
|
@@ -273,6 +279,7 @@ var NetworkMetricsAggregator = class {
|
|
|
273
279
|
*/
|
|
274
280
|
finalizeAggregatedMetric(am) {
|
|
275
281
|
am.avgDurationMs = am.count > 0 ? am.totalDurationMs / am.count : 0;
|
|
282
|
+
am.avgLoadTimeMs = am.count > 0 ? am.totalLoadTimeMs / am.count : 0;
|
|
276
283
|
const samples = this.samplesPerKey.get(am.key) ?? [];
|
|
277
284
|
if (samples.length > 0) {
|
|
278
285
|
samples.sort((a, b) => a - b);
|
|
@@ -314,8 +321,8 @@ function generateHtmlReport(report) {
|
|
|
314
321
|
--text-muted: #7f8c8d;
|
|
315
322
|
--border: #e1e8ed;
|
|
316
323
|
}
|
|
317
|
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: var(--text); margin: 0; padding: 20px; background: var(--bg); }
|
|
318
|
-
.container {
|
|
324
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: var(--text); margin: 0; padding: 20px 80px; background: var(--bg); }
|
|
325
|
+
.container { margin: 0 auto; }
|
|
319
326
|
h1 { color: var(--secondary); margin-bottom: 30px; font-weight: 700; }
|
|
320
327
|
|
|
321
328
|
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
@@ -334,32 +341,24 @@ function generateHtmlReport(report) {
|
|
|
334
341
|
.tab:hover { background: #f8f9fa; }
|
|
335
342
|
.tab.active { background: var(--primary); color: white; border-color: var(--primary); }
|
|
336
343
|
|
|
337
|
-
.
|
|
338
|
-
table {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
344
|
+
.grid-wrapper { background: var(--card-bg); border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); border: 1px solid var(--border); overflow: hidden; }
|
|
345
|
+
.grid-table { overflow-x: auto; }
|
|
346
|
+
.grid-header, .grid-row { display: grid; grid-template-columns: minmax(300px, 4fr) 110px 160px 160px 160px 160px 120px; align-items: center; border-bottom: 1px solid var(--border); }
|
|
347
|
+
.grid-header { background: #f8fafc; font-weight: 700; color: var(--secondary); font-size: 0.85rem; text-transform: uppercase; width: fit-content; min-width: 100%; }
|
|
348
|
+
.grid-cell { padding: 14px 18px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
349
|
+
.grid-header .grid-cell { cursor: pointer; position: relative; display: flex; align-items: center; gap: 4px; }
|
|
350
|
+
.grid-header .grid-cell:hover { background: #f1f5f9; }
|
|
351
|
+
|
|
352
|
+
#grid-body { width: fit-content; min-width: 100%; }
|
|
346
353
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
354
|
+
.grid-row { cursor: pointer; transition: background 0.1s; background: white; }
|
|
355
|
+
.grid-row:hover { background: #fcfdfe; }
|
|
356
|
+
.grid-row.expanded { background: #f8fafc; border-bottom: none; }
|
|
350
357
|
|
|
351
|
-
.
|
|
352
|
-
.
|
|
353
|
-
.
|
|
354
|
-
.PUT { background: #fff3e0; color: #f57c00; }
|
|
355
|
-
.DELETE { background: #ffebee; color: #d32f2f; }
|
|
358
|
+
.details-pane { display: none; background: #f8fafc; border-bottom: 1px solid var(--border); }
|
|
359
|
+
.details-pane.show { display: block; }
|
|
360
|
+
.details-content { padding: 20px; }
|
|
356
361
|
|
|
357
|
-
.error-tag { color: var(--error); font-weight: 700; }
|
|
358
|
-
|
|
359
|
-
/* Details row */
|
|
360
|
-
.details-row { display: none; background: #f8fafc; }
|
|
361
|
-
.details-row.show { display: table-row; }
|
|
362
|
-
.details-content { padding: 20px; border-bottom: 1px solid var(--border); }
|
|
363
362
|
.details-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
|
|
364
363
|
.details-section h4 { margin: 0 0 10px 0; font-size: 0.9rem; color: var(--secondary); border-bottom: 1px solid #dee5ed; padding-bottom: 5px; }
|
|
365
364
|
.details-list { margin: 0; padding: 0; list-style: none; font-size: 0.85rem; }
|
|
@@ -367,14 +366,24 @@ function generateHtmlReport(report) {
|
|
|
367
366
|
.details-list li:last-child { border-bottom: none; }
|
|
368
367
|
.count-badge { background: #edf2f7; padding: 2px 8px; border-radius: 10px; font-weight: 600; color: var(--secondary); }
|
|
369
368
|
|
|
370
|
-
|
|
369
|
+
.endpoint-cell { display: flex; align-items: center; }
|
|
370
|
+
.endpoint-key { overflow: hidden; text-overflow: ellipsis; }
|
|
371
|
+
|
|
372
|
+
.method { font-weight: 800; font-size: 0.7rem; padding: 3px 6px; border-radius: 4px; margin-right: 10px; display: inline-block; min-width: 45px; text-align: center; vertical-align: middle; }
|
|
373
|
+
.GET { background: #e3f2fd; color: #1976d2; }
|
|
374
|
+
.POST { background: #e8f5e9; color: #388e3c; }
|
|
375
|
+
.PUT { background: #fff3e0; color: #f57c00; }
|
|
376
|
+
.DELETE { background: #ffebee; color: #d32f2f; }
|
|
377
|
+
|
|
378
|
+
.error-tag { color: var(--error); font-weight: 700; }
|
|
379
|
+
|
|
371
380
|
.tooltip { position: relative; display: inline-block; margin-left: 4px; cursor: help; color: #cbd5e0; }
|
|
372
381
|
.tooltip:hover { color: var(--primary); }
|
|
373
382
|
.tooltip .tooltiptext { visibility: hidden; width: 220px; background-color: #334155; color: #fff; text-align: center; border-radius: 6px; padding: 8px; position: absolute; z-index: 100; bottom: 125%; left: 50%; margin-left: -110px; opacity: 0; transition: opacity 0.2s; font-size: 0.75rem; text-transform: none; font-weight: 400; line-height: 1.4; pointer-events: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
|
374
383
|
.tooltip:hover .tooltiptext { visibility: visible; opacity: 1; }
|
|
375
384
|
|
|
376
385
|
.chevron { display: inline-block; transition: transform 0.2s; margin-right: 8px; width: 12px; height: 12px; fill: #cbd5e0; }
|
|
377
|
-
|
|
386
|
+
.grid-row.expanded .chevron { transform: rotate(90deg); fill: var(--primary); }
|
|
378
387
|
</style>
|
|
379
388
|
</head>
|
|
380
389
|
<body>
|
|
@@ -411,13 +420,11 @@ function generateHtmlReport(report) {
|
|
|
411
420
|
<div class="tab" data-target="resourceTypes">Resource Types</div>
|
|
412
421
|
</div>
|
|
413
422
|
|
|
414
|
-
<div class="
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
<tbody id="table-body"></tbody>
|
|
420
|
-
</table>
|
|
423
|
+
<div class="grid-wrapper">
|
|
424
|
+
<div class="grid-table" id="metrics-grid">
|
|
425
|
+
<div class="grid-header" id="grid-header"></div>
|
|
426
|
+
<div id="grid-body"></div>
|
|
427
|
+
</div>
|
|
421
428
|
</div>
|
|
422
429
|
</div>
|
|
423
430
|
|
|
@@ -432,11 +439,10 @@ function generateHtmlReport(report) {
|
|
|
432
439
|
const columnMeta = {
|
|
433
440
|
key: { label: 'Endpoint / Key', tooltip: 'The unique identifier for this aggregation group.' },
|
|
434
441
|
count: { label: 'Count', tooltip: 'Total number of requests made.' },
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
totalDurationMs: { label: 'Total Time', tooltip: 'Sum of all response times for this group.' },
|
|
442
|
+
avgLoadTimeMs: { label: 'Avg Load', tooltip: 'Resource Load Time: The time taken to download the resource body (responseEnd - responseStart).' },
|
|
443
|
+
avgDurationMs: { label: 'Avg Duration', tooltip: 'The average response time in milliseconds. This represents the total time from request start to the end of the response body.' },
|
|
444
|
+
totalLoadTimeMs: { label: 'Total Load', tooltip: 'Sum of all load times for this group.' },
|
|
445
|
+
totalDurationMs: { label: 'Total Duration', tooltip: 'Sum of all response times for this group.' },
|
|
440
446
|
errorCount: { label: 'Errors', tooltip: 'Number of failed requests (non-2xx response or network error).' }
|
|
441
447
|
};
|
|
442
448
|
|
|
@@ -480,61 +486,58 @@ function generateHtmlReport(report) {
|
|
|
480
486
|
return (valA - valB) * sortOrder;
|
|
481
487
|
});
|
|
482
488
|
|
|
483
|
-
const header = document.getElementById('
|
|
484
|
-
const body = document.getElementById('
|
|
489
|
+
const header = document.getElementById('grid-header');
|
|
490
|
+
const body = document.getElementById('grid-body');
|
|
485
491
|
|
|
486
|
-
const columns = ['key', 'count', '
|
|
492
|
+
const columns = ['key', 'count', 'avgLoadTimeMs', 'totalLoadTimeMs', 'avgDurationMs', 'totalDurationMs', 'errorCount'];
|
|
487
493
|
|
|
488
494
|
header.innerHTML = columns.map(k => \`
|
|
489
|
-
<
|
|
495
|
+
<div class="grid-cell" onclick="handleSort('\${k}')">
|
|
490
496
|
\${columnMeta[k].label}
|
|
491
|
-
<span class="tooltip"
|
|
497
|
+
<span class="tooltip" title="\${columnMeta[k].tooltip}">\u24D8</span>
|
|
492
498
|
\${sortKey === k ? (sortOrder === 1 ? '\u25B2' : '\u25BC') : ''}
|
|
493
|
-
</
|
|
499
|
+
</div>
|
|
494
500
|
\`).join('');
|
|
495
501
|
|
|
496
502
|
let html = '';
|
|
497
503
|
data.forEach(item => {
|
|
498
504
|
const isExpanded = expandedKeys.has(item.key);
|
|
499
505
|
html += \`
|
|
500
|
-
<
|
|
501
|
-
<
|
|
506
|
+
<div class="grid-row \${isExpanded ? 'expanded' : ''}" onclick="toggleExpand('\${item.key}', event)">
|
|
507
|
+
<div class="grid-cell endpoint-cell">
|
|
502
508
|
<svg class="chevron" viewBox="0 0 20 20"><path d="M7 1L16 10L7 19" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
|
503
509
|
\${item.method ? \`<span class="method \${item.method}">\${item.method}</span>\` : ''}
|
|
504
|
-
\${item.key}
|
|
505
|
-
</
|
|
506
|
-
<
|
|
507
|
-
<
|
|
508
|
-
<
|
|
509
|
-
<
|
|
510
|
-
<
|
|
511
|
-
<
|
|
512
|
-
|
|
513
|
-
</tr>
|
|
510
|
+
<span class="endpoint-key" title="\${item.key}">\${item.method ? item.key.replace(new RegExp(\`^\${item.method}\\\\s+\`), '') : item.key}</span>
|
|
511
|
+
</div>
|
|
512
|
+
<div class="grid-cell">\${item.count}</div>
|
|
513
|
+
<div class="grid-cell">\${formatMs(item.avgLoadTimeMs)}</div>
|
|
514
|
+
<div class="grid-cell">\${formatMs(item.totalLoadTimeMs)}</div>
|
|
515
|
+
<div class="grid-cell">\${formatMs(item.avgDurationMs)}</div>
|
|
516
|
+
<div class="grid-cell">\${formatMs(item.totalDurationMs)}</div>
|
|
517
|
+
<div class="grid-cell \${item.errorCount > 0 ? 'error-tag' : ''}">\${item.errorCount}</div>
|
|
518
|
+
</div>
|
|
514
519
|
\`;
|
|
515
520
|
|
|
516
521
|
if (isExpanded) {
|
|
517
522
|
html += \`
|
|
518
|
-
<
|
|
519
|
-
<
|
|
520
|
-
<div class="details-
|
|
521
|
-
<div class="details-
|
|
522
|
-
<
|
|
523
|
-
|
|
524
|
-
<
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
<
|
|
529
|
-
|
|
530
|
-
<
|
|
531
|
-
|
|
532
|
-
</ul>
|
|
533
|
-
</div>
|
|
523
|
+
<div class="details-pane show">
|
|
524
|
+
<div class="details-content">
|
|
525
|
+
<div class="details-grid">
|
|
526
|
+
<div class="details-section">
|
|
527
|
+
<h4>Contributing Spec Files</h4>
|
|
528
|
+
<ul class="details-list">
|
|
529
|
+
\${item.specs.map(s => \`<li><span>\${s.name}</span> <span class="count-badge">\${s.count}</span></li>\`).join('')}
|
|
530
|
+
</ul>
|
|
531
|
+
</div>
|
|
532
|
+
<div class="details-section">
|
|
533
|
+
<h4>Contributing Tests</h4>
|
|
534
|
+
<ul class="details-list">
|
|
535
|
+
\${item.tests.map(t => \`<li><span>\${t.name}</span> <span class="count-badge">\${t.count}</span></li>\`).join('')}
|
|
536
|
+
</ul>
|
|
534
537
|
</div>
|
|
535
538
|
</div>
|
|
536
|
-
</
|
|
537
|
-
</
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
538
541
|
\`;
|
|
539
542
|
}
|
|
540
543
|
});
|
|
@@ -575,18 +578,23 @@ var NetworkMetricsReporter = class {
|
|
|
575
578
|
* Internal store for all collected request metrics from all tests.
|
|
576
579
|
*/
|
|
577
580
|
allMetrics = [];
|
|
578
|
-
config;
|
|
579
581
|
/**
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
* @param config Configuration for output directory and HTML report generation.
|
|
582
|
+
* Configuration for the plugin.
|
|
583
583
|
*/
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
584
|
+
config = {
|
|
585
|
+
outDir: "playwright-report/network-metrics",
|
|
586
|
+
html: true
|
|
587
|
+
};
|
|
588
|
+
onBegin(config) {
|
|
589
|
+
const reporterEntry = config.reporter.find(
|
|
590
|
+
([name]) => name.includes("playwright-network-metrics")
|
|
591
|
+
);
|
|
592
|
+
if (reporterEntry && typeof reporterEntry[1] === "object") {
|
|
593
|
+
this.config = {
|
|
594
|
+
...this.config,
|
|
595
|
+
...reporterEntry[1]
|
|
596
|
+
};
|
|
597
|
+
}
|
|
590
598
|
}
|
|
591
599
|
/**
|
|
592
600
|
* Playwright lifecycle hook called after each test finishes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright-network-metrics",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Capture and aggregate network performance metrics in Playwright tests with interactive HTML reports.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean --external @playwright/test",
|
|
21
21
|
"lint": "biome check .",
|
|
22
|
-
"lint:fix": "biome check --
|
|
22
|
+
"lint:fix": "biome check --write .",
|
|
23
23
|
"test": "jest",
|
|
24
24
|
"test:pw": "npx playwright test --config playwright-tests/playwright.config.ts",
|
|
25
25
|
"test:watch": "jest --watch"
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@biomejs/biome": "^2.3.10",
|
|
43
43
|
"@playwright/test": "^1.57.0",
|
|
44
|
+
"@types/bun": "^1.3.5",
|
|
44
45
|
"@types/jest": "^30.0.0",
|
|
45
46
|
"@types/micromatch": "^4.0.10",
|
|
46
47
|
"jest": "^30.2.0",
|