feat: add performance testing utilities
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
import { Page } from '@playwright/test';
|
||||||
|
import { CoreWebVitals } from '../../types';
|
||||||
|
|
||||||
|
export class CoreWebVitals {
|
||||||
|
constructor(private page: Page) {}
|
||||||
|
|
||||||
|
async measureLCP(): Promise<number> {
|
||||||
|
return await this.page.evaluate(() => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
new PerformanceObserver((list) => {
|
||||||
|
const entries = list.getEntries();
|
||||||
|
const lastEntry = entries[entries.length - 1];
|
||||||
|
resolve(lastEntry.startTime);
|
||||||
|
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async measureFID(): Promise<number> {
|
||||||
|
return await this.page.evaluate(() => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
new PerformanceObserver((list) => {
|
||||||
|
const entries = list.getEntries();
|
||||||
|
const firstEntry = entries[0];
|
||||||
|
resolve(firstEntry.processingStart - firstEntry.startTime);
|
||||||
|
}).observe({ type: 'first-input', buffered: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async measureCLS(): Promise<number> {
|
||||||
|
return await this.page.evaluate(() => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let clsValue = 0;
|
||||||
|
new PerformanceObserver((list) => {
|
||||||
|
for (const entry of list.getEntries()) {
|
||||||
|
if (!entry.hadRecentInput) {
|
||||||
|
const value = entry.value;
|
||||||
|
clsValue = Math.max(clsValue, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).observe({ type: 'layout-shift', buffered: true });
|
||||||
|
|
||||||
|
setTimeout(() => resolve(clsValue), 5000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async measureAll(): Promise<CoreWebVitals> {
|
||||||
|
const [lcp, fid, cls] = await Promise.all([
|
||||||
|
this.measureLCP(),
|
||||||
|
this.measureFID(),
|
||||||
|
this.measureCLS()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
largestContentfulPaint: lcp,
|
||||||
|
firstInputDelay: fid,
|
||||||
|
cumulativeLayoutShift: cls
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async measureTTFB(): Promise<number> {
|
||||||
|
return await this.page.evaluate(() => {
|
||||||
|
const timing = performance.timing;
|
||||||
|
return timing.responseStart - timing.navigationStart;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async measureFCP(): Promise<number> {
|
||||||
|
return await this.page.evaluate(() => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
new PerformanceObserver((list) => {
|
||||||
|
const entries = list.getEntries();
|
||||||
|
const fcpEntry = entries.find((entry: any) => entry.name === 'first-contentful-paint');
|
||||||
|
if (fcpEntry) {
|
||||||
|
resolve(fcpEntry.startTime);
|
||||||
|
}
|
||||||
|
}).observe({ type: 'paint', buffered: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Page } from '@playwright/test';
|
||||||
|
import { LighthouseResult } from '../../types';
|
||||||
|
|
||||||
|
export class LighthouseRunner {
|
||||||
|
constructor(private page: Page) {}
|
||||||
|
|
||||||
|
async runLighthouse(url: string): Promise<LighthouseResult> {
|
||||||
|
const results = await this.page.evaluate(async () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!(window as any).lighthouse) {
|
||||||
|
resolve({
|
||||||
|
performance: 0,
|
||||||
|
accessibility: 0,
|
||||||
|
bestPractices: 0,
|
||||||
|
seo: 0,
|
||||||
|
pwa: 0
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).lighthouse(url, {
|
||||||
|
onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo', 'pwa']
|
||||||
|
}).then((result: any) => {
|
||||||
|
resolve({
|
||||||
|
performance: Math.round(result.categories.performance.score * 100),
|
||||||
|
accessibility: Math.round(result.categories.accessibility.score * 100),
|
||||||
|
bestPractices: Math.round(result.categories['best-practices'].score * 100),
|
||||||
|
seo: Math.round(result.categories.seo.score * 100),
|
||||||
|
pwa: Math.round(result.categories.pwa.score * 100)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async runPerformanceAudit(): Promise<{
|
||||||
|
score: number;
|
||||||
|
metrics: {
|
||||||
|
firstContentfulPaint: number;
|
||||||
|
largestContentfulPaint: number;
|
||||||
|
cumulativeLayoutShift: number;
|
||||||
|
firstInputDelay: number;
|
||||||
|
speedIndex: number;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
const results = await this.page.evaluate(async () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!(window as any).lighthouse) {
|
||||||
|
resolve({
|
||||||
|
score: 0,
|
||||||
|
metrics: {
|
||||||
|
firstContentfulPaint: 0,
|
||||||
|
largestContentfulPaint: 0,
|
||||||
|
cumulativeLayoutShift: 0,
|
||||||
|
firstInputDelay: 0,
|
||||||
|
speedIndex: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).lighthouse(location.href, {
|
||||||
|
onlyCategories: ['performance']
|
||||||
|
}).then((result: any) => {
|
||||||
|
const audits = result.audits;
|
||||||
|
resolve({
|
||||||
|
score: Math.round(result.categories.performance.score * 100),
|
||||||
|
metrics: {
|
||||||
|
firstContentfulPaint: audits['first-contentful-paint'].numericValue,
|
||||||
|
largestContentfulPaint: audits['largest-contentful-paint'].numericValue,
|
||||||
|
cumulativeLayoutShift: audits['cumulative-layout-shift'].numericValue,
|
||||||
|
firstInputDelay: audits['max-potential-fid'].numericValue,
|
||||||
|
speedIndex: audits['speed-index'].numericValue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Page } from '@playwright/test';
|
||||||
|
import { PerformanceMetrics, NetworkTiming, ResourceTiming } from '../../types';
|
||||||
|
|
||||||
|
export class PerformanceMonitor {
|
||||||
|
constructor(private page: Page) {}
|
||||||
|
|
||||||
|
async measurePageLoad(): Promise<PerformanceMetrics> {
|
||||||
|
const metrics = await this.page.evaluate(() => {
|
||||||
|
const performance = window.performance;
|
||||||
|
const timing = performance.timing;
|
||||||
|
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadTime: timing.loadEventEnd - timing.navigationStart,
|
||||||
|
domContentLoaded: timing.domContentLoadedEventEnd - timing.navigationStart,
|
||||||
|
firstPaint: navigation ? navigation.loadEventEnd - navigation.fetchStart : 0,
|
||||||
|
firstContentfulPaint: navigation ? navigation.domContentLoadedEventEnd - navigation.fetchStart : 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
async measureNetworkTiming(): Promise<NetworkTiming> {
|
||||||
|
return await this.page.evaluate(() => {
|
||||||
|
const timing = performance.timing;
|
||||||
|
return {
|
||||||
|
dns: timing.domainLookupEnd - timing.domainLookupStart,
|
||||||
|
tcp: timing.connectEnd - timing.connectStart,
|
||||||
|
ssl: timing.connectEnd - timing.secureConnectionStart,
|
||||||
|
request: timing.responseStart - timing.requestStart,
|
||||||
|
response: timing.responseEnd - timing.responseStart,
|
||||||
|
total: timing.loadEventEnd - timing.navigationStart,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async measureResourceTiming(): Promise<ResourceTiming[]> {
|
||||||
|
return await this.page.evaluate(() => {
|
||||||
|
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
|
||||||
|
return resources.map(r => ({
|
||||||
|
name: r.name,
|
||||||
|
duration: r.duration,
|
||||||
|
size: r.transferSize,
|
||||||
|
type: r.initiatorType
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async measureMemoryUsage(): Promise<{ usedJSHeapSize: number; totalJSHeapSize: number }> {
|
||||||
|
return await this.page.evaluate(() => {
|
||||||
|
const memory = (performance as any).memory;
|
||||||
|
return {
|
||||||
|
usedJSHeapSize: memory.usedJSHeapSize,
|
||||||
|
totalJSHeapSize: memory.totalJSHeapSize
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user