All files / cwv/_collections cwv-emission.util.ts

100% Statements 121/121
66.66% Branches 4/6
100% Functions 1/1
100% Lines 121/121

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 1221x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 1x  
import { DyE2E_CWVThresholds_Interface } from '../../contracts/_models/interfaces/cwv-thresholds.interface';
import { DyE2E_RouteDescriptor_Interface } from '../../generators/_models/route-descriptor.interface';
import { DyE2E_SpecEmission_Util } from '../../generators/_collections/spec-emission.util';
 
/**
 * Per-route CWV spec-block emission. A kibocsátott Playwright spec:
 *  1. addInitScript(page-script)
 *  2. N iter loop:
 *     - goto(route)
 *     - várja a readyLocator-t
 *     - hide-document (snapshot trigger)
 *     - olvassa __dye2e_cwv_events__-t
 *  3. percentile-aggregálás
 *  4. threshold-assertion
 *  5. JSON-report kiírás
 */
export class DyE2E_CWVEmission_Util {
 
  static emitRouteBlock(
    route: DyE2E_RouteDescriptor_Interface,
    iterations: number,
    thresholds: DyE2E_CWVThresholds_Interface,
    reportDir: string,
  ): string {
    const fullPath: string = route.search ? `${route.path}${route.search}` : route.path;
    const readyWait: string = route.readyLocator
      ? `await page.locator(${DyE2E_SpecEmission_Util.valueLiteral(route.readyLocator)}).first().waitFor({ state: 'visible' });`
      : `await page.waitForLoadState('networkidle');`;
 
    const assertions: string = [
      ['lcp', thresholds.lcp_p75_ms, 'ms'],
      ['cls', thresholds.cls_p75, ''],
      ['inp', thresholds.inp_p75_ms, 'ms'],
      ['fcp', thresholds.fcp_p75_ms, 'ms'],
      ['ttfb', thresholds.ttfb_p75_ms, 'ms'],
    ]
      .filter((e: [string, number | undefined, string]): boolean => typeof e[1] === 'number')
      .map(([key, threshold, unit]: [string, number | undefined, string]): string =>
        `if (summary['${key}'].n > 0) {\n  expect(summary['${key}'].p75, 'p75 ${key.toUpperCase()} threshold ${threshold}${unit}').toBeLessThanOrEqual(${threshold});\n}`,
      )
      .join('\n');
 
    return `test('CWV: ${DyE2E_SpecEmission_Util.escapeString(route.label ?? route.key)} (${DyE2E_SpecEmission_Util.escapeString(route.path)})', async ({ page }): Promise<void> => {
  const measurements: { metric: string; value: number; iter: number }[] = [];
 
  for (let iter: number = 0; iter < ${iterations}; iter++) {
    await page.addInitScript({ content: DYE2E_WEB_VITALS_PAGE_SCRIPT });
    if (typeof DYE2E_INP_PAGE_SCRIPT !== 'undefined') {
      await page.addInitScript({ content: DYE2E_INP_PAGE_SCRIPT });
    }
    const response = await page.goto(${DyE2E_SpecEmission_Util.valueLiteral(fullPath)});
    expect(response?.ok()).toBeTruthy();
    ${readyWait}
    // Wave-3 INP interaction-driver — szintetikus interakciók, hogy INP mérhető legyen.
    // Az INP page-script interactionId-vel rendelkező event-eket figyel.
    if (typeof DYE2E_INP_PAGE_SCRIPT !== 'undefined') {
      // 3 interakció — click + Tab + Enter; biztonsági settle-wait
      try {
        await page.mouse.click(50, 50, { delay: 10 });
        await page.waitForTimeout(50);
        await page.keyboard.press('Tab');
        await page.waitForTimeout(50);
        await page.keyboard.press('Enter');
        await page.waitForTimeout(50);
      } catch (e: unknown) { /* swallow — interaction-target may not exist */ }
    }
    // Force snapshot — trigger pagehide via page.evaluate (LCP/CLS + opcionálisan INP)
    await page.evaluate((): void => {
      const w = window as any;
      if (typeof w.__dye2e_cwv_snapshot__ === 'function') { w.__dye2e_cwv_snapshot__(); }
      if (typeof w.__dye2e_inp_snapshot__ === 'function') { w.__dye2e_inp_snapshot__(); }
    });
    const events: any[] = await page.evaluate(() => (window as any).__dye2e_cwv_events__ || []);
    for (const ev of events) {
      measurements.push({ metric: ev.metric, value: ev.value, iter: iter });
    }
  }
 
  function pct(values: number[], p: number): number {
    if (values.length === 0) { return NaN; }
    const sorted: number[] = [...values].sort((a: number, b: number): number => a - b);
    if (sorted.length === 1) { return sorted[0]; }
    const rank: number = p * (sorted.length - 1);
    const lo: number = Math.floor(rank);
    const hi: number = Math.ceil(rank);
    if (lo === hi) { return sorted[lo]; }
    const frac: number = rank - lo;
    return sorted[lo] * (1 - frac) + sorted[hi] * frac;
  }
 
  const metrics: string[] = ['lcp', 'cls', 'inp', 'ttfb', 'fcp'];
  const summary: Record<string, { n: number; p50: number; p75: number; p95: number; min: number; max: number; mean: number }> = {} as any;
  for (const m of metrics) {
    const values: number[] = measurements.filter((x): boolean => x.metric === m).map((x): number => x.value);
    const n: number = values.length;
    const sum: number = values.reduce((acc: number, v: number): number => acc + v, 0);
    summary[m] = {
      n: n,
      p50: pct(values, 0.5),
      p75: pct(values, 0.75),
      p95: pct(values, 0.95),
      min: n > 0 ? Math.min(...values) : NaN,
      max: n > 0 ? Math.max(...values) : NaN,
      mean: n > 0 ? sum / n : NaN,
    };
  }
 
  // JSON report
  const fs = require('fs');
  const path = require('path');
  const reportDir: string = ${DyE2E_SpecEmission_Util.valueLiteral(reportDir)};
  fs.mkdirSync(reportDir, { recursive: true });
  fs.writeFileSync(
    path.join(reportDir, ${DyE2E_SpecEmission_Util.valueLiteral(`${route.key}.json`)}),
    JSON.stringify({ route: ${DyE2E_SpecEmission_Util.valueLiteral(route.key)}, summary: summary, raw: measurements }, null, 2),
  );
 
  ${assertions}
});`;
  }
}