@piwitests/reporter 0.60.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1678,7 +1678,7 @@ var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "dat
1678
1678
  function generateAlternatives(attrs, options = {}) {
1679
1679
  const alts = [];
1680
1680
  const seen = /* @__PURE__ */ new Set();
1681
- const add = (loc) => {
1681
+ const add2 = (loc) => {
1682
1682
  if (!seen.has(loc.locator)) {
1683
1683
  seen.add(loc.locator);
1684
1684
  alts.push(loc);
@@ -1695,7 +1695,7 @@ function generateAlternatives(attrs, options = {}) {
1695
1695
  const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
1696
1696
  const testId = attr(attrs, testIdAttribute);
1697
1697
  if (testId && isUnique(counts?.testId)) {
1698
- add({
1698
+ add2({
1699
1699
  locator: `getByTestId(${quoteJsString(testId)})`,
1700
1700
  method: "getByTestId",
1701
1701
  args: { testId },
@@ -1706,7 +1706,7 @@ function generateAlternatives(attrs, options = {}) {
1706
1706
  const levelPart = level != null ? `, level: ${level}` : "";
1707
1707
  const withLevel = (base) => level != null ? { ...base, level } : base;
1708
1708
  if (role && accessibleName) {
1709
- add({
1709
+ add2({
1710
1710
  locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
1711
1711
  method: "getByRole",
1712
1712
  args: withLevel({ role, name: accessibleName }),
@@ -1715,7 +1715,7 @@ function generateAlternatives(attrs, options = {}) {
1715
1715
  }
1716
1716
  const ariaLabel = attr(attrs, "aria-label");
1717
1717
  if (role && ariaLabel && ariaLabel !== accessibleName) {
1718
- add({
1718
+ add2({
1719
1719
  locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
1720
1720
  method: "getByRole",
1721
1721
  args: withLevel({ role, name: ariaLabel }),
@@ -1726,7 +1726,7 @@ function generateAlternatives(attrs, options = {}) {
1726
1726
  const label = attr(attrs, "aria-label");
1727
1727
  const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
1728
1728
  if (labelBacked) {
1729
- add({
1729
+ add2({
1730
1730
  locator: `getByLabel(${quoteJsString(accessibleName)})`,
1731
1731
  method: "getByLabel",
1732
1732
  args: { label: accessibleName },
@@ -1736,7 +1736,7 @@ function generateAlternatives(attrs, options = {}) {
1736
1736
  }
1737
1737
  const placeholder = attr(attrs, "placeholder");
1738
1738
  if (placeholder) {
1739
- add({
1739
+ add2({
1740
1740
  locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
1741
1741
  method: "getByPlaceholder",
1742
1742
  args: { placeholder },
@@ -1744,7 +1744,7 @@ function generateAlternatives(attrs, options = {}) {
1744
1744
  });
1745
1745
  }
1746
1746
  if (text && text.length < 80) {
1747
- add({
1747
+ add2({
1748
1748
  locator: `getByText(${quoteJsString(text)})`,
1749
1749
  method: "getByText",
1750
1750
  args: { text },
@@ -1754,7 +1754,7 @@ function generateAlternatives(attrs, options = {}) {
1754
1754
  const id = attr(attrs, "id");
1755
1755
  if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
1756
1756
  const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
1757
- add({
1757
+ add2({
1758
1758
  locator: `locator(${quoteJsString(selector)})`,
1759
1759
  method: "locator",
1760
1760
  args: { selector },
@@ -1764,7 +1764,7 @@ function generateAlternatives(attrs, options = {}) {
1764
1764
  const name = attr(attrs, "name");
1765
1765
  if (name && selectable("name") && isUnique(counts?.name)) {
1766
1766
  const selector = `[name="${escCssAttrValue(name)}"]`;
1767
- add({
1767
+ add2({
1768
1768
  locator: `locator(${quoteJsString(selector)})`,
1769
1769
  method: "locator",
1770
1770
  args: { selector },
@@ -1773,7 +1773,7 @@ function generateAlternatives(attrs, options = {}) {
1773
1773
  }
1774
1774
  const alt = attr(attrs, "alt");
1775
1775
  if (alt) {
1776
- add({
1776
+ add2({
1777
1777
  locator: `getByAltText(${quoteJsString(alt)})`,
1778
1778
  method: "getByAltText",
1779
1779
  args: { text: alt },
@@ -1782,7 +1782,7 @@ function generateAlternatives(attrs, options = {}) {
1782
1782
  }
1783
1783
  const title = attr(attrs, "title");
1784
1784
  if (title) {
1785
- add({
1785
+ add2({
1786
1786
  locator: `getByTitle(${quoteJsString(title)})`,
1787
1787
  method: "getByTitle",
1788
1788
  args: { title },
@@ -1800,7 +1800,7 @@ function generateAlternatives(attrs, options = {}) {
1800
1800
  if (scopedCount(anc) !== 1) continue;
1801
1801
  if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
1802
1802
  testIdAnchorDone = true;
1803
- add({
1803
+ add2({
1804
1804
  locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
1805
1805
  method: leaf.method,
1806
1806
  args: { ...leaf.args, anchorTestId: anc.testId },
@@ -1810,7 +1810,7 @@ function generateAlternatives(attrs, options = {}) {
1810
1810
  if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
1811
1811
  idAnchorDone = true;
1812
1812
  const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
1813
- add({
1813
+ add2({
1814
1814
  locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
1815
1815
  method: leaf.method,
1816
1816
  args: { ...leaf.args, anchorSelector },
@@ -1821,7 +1821,7 @@ function generateAlternatives(attrs, options = {}) {
1821
1821
  dataAnchorDone = true;
1822
1822
  const { name: name2, value } = anc.dataAttr;
1823
1823
  const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
1824
- add({
1824
+ add2({
1825
1825
  locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
1826
1826
  method: leaf.method,
1827
1827
  args: { ...leaf.args, anchorSelector },
@@ -1831,7 +1831,7 @@ function generateAlternatives(attrs, options = {}) {
1831
1831
  const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
1832
1832
  if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
1833
1833
  roleAnchorDone = true;
1834
- add({
1834
+ add2({
1835
1835
  locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
1836
1836
  method: leaf.method,
1837
1837
  args: { ...leaf.args, anchorRole: ancestorRole },
@@ -1840,7 +1840,7 @@ function generateAlternatives(attrs, options = {}) {
1840
1840
  }
1841
1841
  if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
1842
1842
  filterAnchorDone = true;
1843
- add({
1843
+ add2({
1844
1844
  locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
1845
1845
  method: leaf.method,
1846
1846
  args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
@@ -1859,7 +1859,7 @@ function generateAlternatives(attrs, options = {}) {
1859
1859
  );
1860
1860
  const pos = attrs.rolePosition;
1861
1861
  if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
1862
- add({
1862
+ add2({
1863
1863
  locator: `getByRole(${rolePart})`,
1864
1864
  method: "getByRole",
1865
1865
  args: leafArgs,
@@ -1878,7 +1878,7 @@ function generateAlternatives(attrs, options = {}) {
1878
1878
  const countsClasses = counts?.classes;
1879
1879
  const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
1880
1880
  for (const { cls, score } of classes) {
1881
- add({
1881
+ add2({
1882
1882
  locator: `locator(${quoteJsString(`.${cls}`)})`,
1883
1883
  method: "locator",
1884
1884
  args: { selector: `.${cls}` },
@@ -1935,7 +1935,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
1935
1935
  const seen = /* @__PURE__ */ new Set();
1936
1936
  const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
1937
1937
  const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
1938
- const add = (l) => {
1938
+ const add2 = (l) => {
1939
1939
  if (!seen.has(l.locator)) {
1940
1940
  seen.add(l.locator);
1941
1941
  out.push(l);
@@ -1945,7 +1945,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
1945
1945
  if (anchor.scopedLeafCount !== 1) continue;
1946
1946
  const seg = anchorSegment(anchor);
1947
1947
  if (!seg) continue;
1948
- add({
1948
+ add2({
1949
1949
  locator: `${seg.code}.getByRole(${rolePart})`,
1950
1950
  method: "getByRole",
1951
1951
  args: { ...leafArgs, ...seg.flatArgs },
@@ -1958,7 +1958,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
1958
1958
  if (segments.every((s) => s !== null)) {
1959
1959
  const innermost = segments[segments.length - 1];
1960
1960
  const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
1961
- add({
1961
+ add2({
1962
1962
  locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
1963
1963
  method: "getByRole",
1964
1964
  // Flat args describe the innermost anchor (what the healing fingerprint
@@ -2401,6 +2401,8 @@ var ATTACHMENT_NAMES = {
2401
2401
  codeReach: "piwi-code-reach",
2402
2402
  browserCoverage: "piwi-browser-coverage",
2403
2403
  browserFunctions: "piwi-browser-functions",
2404
+ cssCoverage: "piwi-css-coverage",
2405
+ cssRules: "piwi-css-rules",
2404
2406
  serverCoverageRequests: "piwi-server-coverage-requests",
2405
2407
  webVitals: "piwi-web-vitals",
2406
2408
  locatorSuggestion: "piwi-locator-suggestion",
@@ -2718,6 +2720,7 @@ var PIWI_ENV_KEYS = {
2718
2720
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
2719
2721
  coverageBrowser: "PIWI_COVERAGE_BROWSER",
2720
2722
  coverageServer: "PIWI_COVERAGE_SERVER",
2723
+ coverageCss: "PIWI_COVERAGE_CSS",
2721
2724
  externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
2722
2725
  externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
2723
2726
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
@@ -3807,10 +3810,10 @@ function joinRoot(root, source) {
3807
3810
  function decodeLines(raw) {
3808
3811
  const sources = [];
3809
3812
  const decoded = [];
3810
- const add = (map, lineOffset, columnOffset) => {
3813
+ const add2 = (map, lineOffset, columnOffset) => {
3811
3814
  if (map.sections) {
3812
3815
  for (const section of map.sections) {
3813
- if (!add(section.map, section.offset.line, section.offset.column)) return false;
3816
+ if (!add2(section.map, section.offset.line, section.offset.column)) return false;
3814
3817
  }
3815
3818
  return true;
3816
3819
  }
@@ -3831,7 +3834,7 @@ function decodeLines(raw) {
3831
3834
  });
3832
3835
  return true;
3833
3836
  };
3834
- return add(raw, 0, 0) ? { sources, lines: decoded } : null;
3837
+ return add2(raw, 0, 0) ? { sources, lines: decoded } : null;
3835
3838
  }
3836
3839
  function compactSegments(lines) {
3837
3840
  let total = 0;
@@ -4007,7 +4010,7 @@ function functionLines(entry, map, text, all) {
4007
4010
  const read = textOf(entry, text);
4008
4011
  const ranges = executedFunctionRanges(entry, read?.length, all);
4009
4012
  if (!ranges.length || !read) return out;
4010
- const add = (source, range) => {
4013
+ const add2 = (source, range) => {
4011
4014
  const list = out.get(source);
4012
4015
  if (list) list.push(range);
4013
4016
  else out.set(source, [range]);
@@ -4017,7 +4020,7 @@ function functionLines(entry, map, text, all) {
4017
4020
  const start = offsetToPosition(starts, range.start);
4018
4021
  if (!map) {
4019
4022
  const last = offsetToPosition(starts, Math.max(range.start, range.end - 1));
4020
- add(-1, [start.line + 1, last.line + 1]);
4023
+ add2(-1, [start.line + 1, last.line + 1]);
4021
4024
  continue;
4022
4025
  }
4023
4026
  const at = map.originalAt(start.line, start.column);
@@ -4027,7 +4030,7 @@ function functionLines(entry, map, text, all) {
4027
4030
  if (first === null) continue;
4028
4031
  const header = first === at.line || first === at.line + 1 ? at.line : first;
4029
4032
  const lines = map.originalLinesBetween(at.source, start, end, header);
4030
- if (lines) add(at.source, [header + 1, lines[1] + 1]);
4033
+ if (lines) add2(at.source, [header + 1, lines[1] + 1]);
4031
4034
  }
4032
4035
  return out;
4033
4036
  }
@@ -4934,6 +4937,9 @@ var SERVER_VALUES = /* @__PURE__ */ new Set(["files", "functions", "lines"]);
4934
4937
  function isServerGranularity(value) {
4935
4938
  return typeof value === "string" && SERVER_VALUES.has(value);
4936
4939
  }
4940
+ function cssCoverageInWorker() {
4941
+ return process.env[PIWI_ENV_KEYS.coverageCss] === "rules";
4942
+ }
4937
4943
  function browserCoverageInWorker() {
4938
4944
  const browser = process.env[PIWI_ENV_KEYS.coverageBrowser];
4939
4945
  return process.env[PIWI_ENV_KEYS.captureCodeReach] === "true" || browser === "files" || browser === "functions";
@@ -5215,7 +5221,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
5215
5221
  if (listed) listed.push(...functions);
5216
5222
  else into.set(path15, [...functions]);
5217
5223
  };
5218
- const add = (file, functions) => addTo(files, file, functions);
5224
+ const add2 = (file, functions) => addTo(files, file, functions);
5219
5225
  const bases = viteBases(entries);
5220
5226
  const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
5221
5227
  const cached = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
@@ -5242,14 +5248,14 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
5242
5248
  const text = textOf2(covered, resolution);
5243
5249
  if (resolution.lookup) {
5244
5250
  const reached = reachedFunctionLines(covered, resolution.lookup, text);
5245
- if (reached.size > 0) add(resolution.file, [...reached.values()].flat());
5251
+ if (reached.size > 0) add2(resolution.file, [...reached.values()].flat());
5246
5252
  } else if (scriptRan(covered, text)) {
5247
- add(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
5253
+ add2(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
5248
5254
  }
5249
5255
  } else if (resolution.kind === "map") {
5250
5256
  for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup, textOf2(covered, resolution))) {
5251
5257
  const file = mapFile(resolution, index);
5252
- if (file) add(file, functions);
5258
+ if (file) add2(file, functions);
5253
5259
  }
5254
5260
  }
5255
5261
  if (!withKnown || resolution.kind === "none") continue;
@@ -5328,10 +5334,13 @@ async function markCurrentDocument(page, cdp) {
5328
5334
  }
5329
5335
  function onPaused(page, capture, event) {
5330
5336
  const leaving = event.reason === "EventListener" && event.data?.eventName === "listener:beforeunload" && (event.callFrames?.[0]?.functionName ?? MARK_FUNCTION) === MARK_FUNCTION;
5331
- const read = leaving ? readCoverage(page, capture.cdp) : null;
5337
+ const read = leaving ? Promise.all([
5338
+ capture.js ? readCoverage(page, capture.cdp) : Promise.resolve([]),
5339
+ capture.css ? readCssUsage(page, capture.cdp, capture.css) : Promise.resolve()
5340
+ ]) : null;
5332
5341
  if (read) {
5333
5342
  const merging = read.then(
5334
- (coverage) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
5343
+ ([coverage]) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
5335
5344
  );
5336
5345
  capture.reading.add(merging);
5337
5346
  void merging.finally(() => capture.reading.delete(merging));
@@ -5345,7 +5354,7 @@ function versionKey(url, hash) {
5345
5354
  return `${hash} ${url}`;
5346
5355
  }
5347
5356
  function onScriptParsed(page, capture, event) {
5348
- if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
5357
+ if (!capture.js || !event.url || REPLAYED_SCRIPT.test(event.url)) return;
5349
5358
  capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash, pageUrl: capture.pageUrl });
5350
5359
  if (NEVER_REACHED.test(event.url)) return;
5351
5360
  const key = versionKey(event.url, event.hash);
@@ -5374,6 +5383,53 @@ function readCoverage(page, cdp) {
5374
5383
  );
5375
5384
  return Promise.race([taken, late]).finally(() => clearTimeout(timer));
5376
5385
  }
5386
+ function onStyleSheetAdded(page, cdp, css, pageUrl, header) {
5387
+ if (header.isConstructed) return;
5388
+ const sheet = {
5389
+ sourceURL: header.sourceURL,
5390
+ ...header.sourceMapURL ? { sourceMapURL: header.sourceMapURL } : {},
5391
+ isInline: header.isInline,
5392
+ startLine: header.startLine,
5393
+ pageUrl,
5394
+ used: /* @__PURE__ */ new Map()
5395
+ };
5396
+ css.sheets.set(header.styleSheetId, sheet);
5397
+ const reading = (async () => {
5398
+ try {
5399
+ if (header.ownerNode !== void 0 && !/\.css(?:[?#]|$)/i.test(header.sourceURL)) {
5400
+ const { node } = await internalCall(
5401
+ page,
5402
+ () => cdp.send("DOM.describeNode", { backendNodeId: header.ownerNode })
5403
+ );
5404
+ const attributes = node.attributes ?? [];
5405
+ const at = attributes.indexOf("data-vite-dev-id");
5406
+ if (at >= 0 && typeof attributes[at + 1] === "string") sheet.devId = attributes[at + 1];
5407
+ }
5408
+ if (/\/node_modules\//.test(sheet.devId ?? header.sourceURL)) return;
5409
+ const { text } = await internalCall(
5410
+ page,
5411
+ () => cdp.send("CSS.getStyleSheetText", { styleSheetId: header.styleSheetId })
5412
+ );
5413
+ sheet.text = text;
5414
+ } catch {
5415
+ }
5416
+ })();
5417
+ css.reading.add(reading);
5418
+ void reading.finally(() => css.reading.delete(reading));
5419
+ }
5420
+ async function readCssUsage(page, cdp, css) {
5421
+ try {
5422
+ const { coverage } = await internalCall(page, () => cdp.send("CSS.takeCoverageDelta"));
5423
+ for (const rule of coverage) {
5424
+ if (!rule.used) continue;
5425
+ css.sheets.get(rule.styleSheetId)?.used.set(`${rule.startOffset}:${rule.endOffset}`, {
5426
+ startOffset: rule.startOffset,
5427
+ endOffset: rule.endOffset
5428
+ });
5429
+ }
5430
+ } catch {
5431
+ }
5432
+ }
5377
5433
  function mergeCoverageRead(ran, scripts, read, keepIdle = true) {
5378
5434
  for (const covered of read) {
5379
5435
  const script = scripts.get(covered.scriptId);
@@ -5408,11 +5464,15 @@ async function detach(page, cdp) {
5408
5464
  }
5409
5465
  async function startCodeReach(page) {
5410
5466
  if (!isChromiumPage(page) || captures.has(page)) return false;
5467
+ const withCss = cssCoverageInWorker();
5468
+ const js = browserCoverageInWorker() || !withCss;
5411
5469
  let cdp = null;
5412
5470
  try {
5413
5471
  cdp = await internalCall(page, () => page.context().newCDPSession(page));
5414
5472
  const capture = {
5415
5473
  cdp,
5474
+ js,
5475
+ css: withCss ? { sheets: /* @__PURE__ */ new Map(), reading: /* @__PURE__ */ new Set() } : null,
5416
5476
  scripts: /* @__PURE__ */ new Map(),
5417
5477
  sources: /* @__PURE__ */ new Map(),
5418
5478
  ran: /* @__PURE__ */ new Map(),
@@ -5422,6 +5482,9 @@ async function startCodeReach(page) {
5422
5482
  };
5423
5483
  const marks = marksOf(page);
5424
5484
  cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
5485
+ const css = capture.css;
5486
+ if (css)
5487
+ cdp.on("CSS.styleSheetAdded", (event) => onStyleSheetAdded(page, cdp, css, capture.pageUrl, event.header));
5425
5488
  cdp.on("Debugger.paused", (event) => onPaused(page, capture, event));
5426
5489
  cdp.on("Page.frameNavigated", (event) => {
5427
5490
  if (event.frame.parentId) return;
@@ -5432,8 +5495,17 @@ async function startCodeReach(page) {
5432
5495
  page,
5433
5496
  () => Promise.all([
5434
5497
  capture.cdp.send("Debugger.enable"),
5435
- capture.cdp.send("Profiler.enable"),
5436
- capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false }),
5498
+ ...js ? [
5499
+ capture.cdp.send("Profiler.enable"),
5500
+ capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
5501
+ ] : [],
5502
+ // The style sheets' rule usage records nothing without the DOM and CSS domains. A failure there leaves the
5503
+ // scripts' coverage running.
5504
+ ...css ? [
5505
+ capture.cdp.send("DOM.enable").then(() => capture.cdp.send("CSS.enable")).then(() => capture.cdp.send("CSS.startRuleUsageTracking")).catch(() => {
5506
+ capture.css = null;
5507
+ })
5508
+ ] : [],
5437
5509
  capture.cdp.send("Page.enable"),
5438
5510
  capture.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: MARK_SOURCE, worldName: MARK_WORLD }),
5439
5511
  capture.cdp.send("DOMDebugger.setEventListenerBreakpoint", { eventName: "beforeunload" })
@@ -5448,17 +5520,28 @@ async function startCodeReach(page) {
5448
5520
  }
5449
5521
  }
5450
5522
  async function stopCodeReach(page) {
5523
+ return (await stopPageCoverage(page)).scripts;
5524
+ }
5525
+ async function stopPageCoverage(page) {
5451
5526
  const capture = captures.get(page);
5452
- if (!capture) return null;
5527
+ if (!capture) return { scripts: null, sheets: [] };
5453
5528
  captures.delete(page);
5454
5529
  const { cdp } = capture;
5455
- try {
5530
+ const sheets = async () => {
5531
+ if (!capture.css) return [];
5532
+ await readCssUsage(page, cdp, capture.css);
5533
+ await Promise.all(capture.css.reading);
5534
+ await internalCall(page, () => cdp.send("CSS.stopRuleUsageTracking")).catch(() => {
5535
+ });
5536
+ return [...capture.css.sheets.values()].filter((sheet) => sheet.used.size > 0 || sheet.text !== void 0);
5537
+ };
5538
+ const scripts = async () => {
5539
+ if (!capture.js) return null;
5456
5540
  const last = await internalCall(page, async () => {
5457
5541
  const taken = await cdp.send("Profiler.takePreciseCoverage");
5458
5542
  await cdp.send("Profiler.stopPreciseCoverage");
5459
5543
  return taken.result;
5460
5544
  });
5461
- await Promise.all(capture.reading);
5462
5545
  mergeCoverageRead(capture.ran, capture.scripts, last, capture.keepIdle);
5463
5546
  const entries = [];
5464
5547
  for (const [key, { script, functions }] of capture.ran) {
@@ -5468,15 +5551,279 @@ async function stopCodeReach(page) {
5468
5551
  entries.push(entry);
5469
5552
  }
5470
5553
  return entries;
5471
- } catch {
5472
- return null;
5554
+ };
5555
+ try {
5556
+ await Promise.all(capture.reading);
5557
+ const [read, styles] = await Promise.all([scripts().catch(() => null), sheets().catch(() => [])]);
5558
+ return { scripts: read, sheets: styles };
5473
5559
  } finally {
5474
5560
  await detach(page, cdp);
5475
5561
  }
5476
5562
  }
5477
5563
 
5564
+ // src/internal/capture/css-coverage.ts
5565
+ var crypto3 = __toESM(require("crypto"));
5566
+ var fs9 = __toESM(require("fs"));
5567
+
5568
+ // ../core/src/css-rules.ts
5569
+ var GROUPING = /* @__PURE__ */ new Set(["media", "supports", "layer", "container", "scope", "starting-style", "document"]);
5570
+ function parseCssRules(text) {
5571
+ const rules = [];
5572
+ const open = [];
5573
+ let preludeStart = -1;
5574
+ for (let i = 0; i < text.length; i++) {
5575
+ const c = text[i];
5576
+ if (c === "/" && text[i + 1] === "*") {
5577
+ const end = text.indexOf("*/", i + 2);
5578
+ i = end < 0 ? text.length : end + 1;
5579
+ continue;
5580
+ }
5581
+ if (c === '"' || c === "'") {
5582
+ if (preludeStart < 0) preludeStart = i;
5583
+ for (i++; i < text.length && text[i] !== c && text[i] !== "\n"; i++) if (text[i] === "\\") i++;
5584
+ continue;
5585
+ }
5586
+ if (c === "\\") {
5587
+ if (preludeStart < 0) preludeStart = i;
5588
+ i++;
5589
+ continue;
5590
+ }
5591
+ if ((c === "u" || c === "U") && /^url\(/i.test(text.slice(i, i + 4)) && !/[\w-]/.test(text[i - 1] ?? "")) {
5592
+ let j = i + 4;
5593
+ while (j < text.length && /\s/.test(text[j])) j++;
5594
+ if (text[j] !== '"' && text[j] !== "'") {
5595
+ if (preludeStart < 0) preludeStart = i;
5596
+ for (; j < text.length && text[j] !== ")"; j++) if (text[j] === "\\") j++;
5597
+ i = j;
5598
+ continue;
5599
+ }
5600
+ }
5601
+ if (text.startsWith("<!--", i) || text.startsWith("-->", i)) {
5602
+ i += text.startsWith("<!--", i) ? 3 : 2;
5603
+ continue;
5604
+ }
5605
+ if (c === "{") {
5606
+ const start = preludeStart < 0 ? i : preludeStart;
5607
+ const atRule = /^@([\w-]+)/.exec(text.slice(start, i));
5608
+ const parent = open[open.length - 1];
5609
+ const kind = atRule ? GROUPING.has(atRule[1].toLowerCase()) ? "group" : "other" : parent?.kind === "other" ? "other" : "style";
5610
+ open.push({ kind, start, open: i });
5611
+ preludeStart = -1;
5612
+ continue;
5613
+ }
5614
+ if (c === "}") {
5615
+ const block = open.pop();
5616
+ if (block && block.kind !== "other")
5617
+ rules.push({ kind: block.kind, start: block.start, open: block.open, end: i + 1 });
5618
+ preludeStart = -1;
5619
+ continue;
5620
+ }
5621
+ if (c === ";") {
5622
+ preludeStart = -1;
5623
+ continue;
5624
+ }
5625
+ if (preludeStart < 0 && !/\s/.test(c)) preludeStart = i;
5626
+ }
5627
+ for (const block of open) {
5628
+ if (block.kind !== "other")
5629
+ rules.push({ kind: block.kind, start: block.start, open: block.open, end: text.length });
5630
+ }
5631
+ return rules.sort((a, b) => a.start - b.start);
5632
+ }
5633
+ function usedCssRules(rules, used) {
5634
+ const styleAt = /* @__PURE__ */ new Map();
5635
+ const groups = [];
5636
+ for (const rule of rules) {
5637
+ if (rule.kind === "style") styleAt.set(rule.start, rule);
5638
+ else groups.push(rule);
5639
+ }
5640
+ const out = /* @__PURE__ */ new Map();
5641
+ for (const range of used) {
5642
+ const style = styleAt.get(range.startOffset);
5643
+ if (style) {
5644
+ out.set(`${style.start}`, [style.start, style.end]);
5645
+ continue;
5646
+ }
5647
+ const group = groups.find((rule) => rule.start <= range.startOffset && range.startOffset <= rule.open);
5648
+ if (group) out.set(`${group.start}`, [group.start, group.start + 1]);
5649
+ }
5650
+ return [...out.values()].sort((a, b) => a[0] - b[0]);
5651
+ }
5652
+ function allCssRules(rules) {
5653
+ return rules.map((rule) => rule.kind === "style" ? [rule.start, rule.end] : [rule.start, rule.start + 1]);
5654
+ }
5655
+ function cssRuleLines(text, offsets, options = {}) {
5656
+ const starts = lineStarts(text);
5657
+ const lineOffset = options.lineOffset ?? 0;
5658
+ const out = /* @__PURE__ */ new Map();
5659
+ const add2 = (source, range) => {
5660
+ const ranges = out.get(source);
5661
+ if (ranges) ranges.push(range);
5662
+ else out.set(source, [range]);
5663
+ };
5664
+ for (const [startOffset, endOffset] of offsets) {
5665
+ const start = offsetToPosition(starts, startOffset);
5666
+ const end = offsetToPosition(starts, Math.max(startOffset, endOffset - 1));
5667
+ if (!options.lookup) {
5668
+ add2(-1, [start.line + lineOffset + 1, end.line + lineOffset + 1]);
5669
+ continue;
5670
+ }
5671
+ const first = options.lookup.originalAt(start.line, start.column);
5672
+ if (!first) continue;
5673
+ const last = options.lookup.originalAt(end.line, end.column);
5674
+ const lastLine = last && last.source === first.source && last.line >= first.line ? last.line : first.line;
5675
+ add2(first.source, [first.line + 1, lastLine + 1]);
5676
+ }
5677
+ return out;
5678
+ }
5679
+
5680
+ // src/internal/capture/css-coverage.ts
5681
+ var STYLE_SOURCES = /\.(?:css|scss|sass|less|styl|stylus|pcss|postcss|vue|svelte|astro|html?)$/i;
5682
+ var BUILD_DIRS2 = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit)\//;
5683
+ var resolutions = /* @__PURE__ */ new Map();
5684
+ var MAX_CACHED_SHEETS = 2e3;
5685
+ function fileForStyleUrl(url, roots) {
5686
+ let pathname;
5687
+ try {
5688
+ const parsed2 = new URL(url);
5689
+ if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
5690
+ pathname = parsed2.pathname;
5691
+ } catch {
5692
+ return null;
5693
+ }
5694
+ return sourceFile(decodeURIComponent(pathname), roots);
5695
+ }
5696
+ function sourceFile(source, roots) {
5697
+ const bare = source.replace(/[?#].*$/, "");
5698
+ if (!STYLE_SOURCES.test(bare) || /(?:^|\/)node_modules\//.test(bare)) return null;
5699
+ const file = resolveSourcePath(bare, roots);
5700
+ if (!file || BUILD_DIRS2.test(file.replace(/\\/g, "/")) || /(?:^|[\\/])node_modules[\\/]/.test(file)) return null;
5701
+ return file;
5702
+ }
5703
+ function styleBlockLine(file, index) {
5704
+ let text;
5705
+ try {
5706
+ text = fs9.readFileSync(file, "utf-8");
5707
+ } catch {
5708
+ return 0;
5709
+ }
5710
+ const blocks = [...text.matchAll(/^<style\b[^>]*>/gim)];
5711
+ const block = blocks[index];
5712
+ if (!block) return 0;
5713
+ return text.slice(0, block.index + block[0].length).split("\n").length - 1;
5714
+ }
5715
+ async function sheetMap(sheet, text, fetchMap) {
5716
+ const ref = sourceMappingUrl(text) ?? sheet.sourceMapURL ?? null;
5717
+ if (!ref) return null;
5718
+ let raw;
5719
+ if (ref.startsWith("data:")) raw = decodeDataUrl(ref);
5720
+ else {
5721
+ try {
5722
+ raw = await fetchMap(new URL(ref, sheet.sourceURL).href);
5723
+ } catch {
5724
+ raw = null;
5725
+ }
5726
+ }
5727
+ if (!raw) return null;
5728
+ try {
5729
+ return decodeSourceMap(JSON.parse(raw));
5730
+ } catch {
5731
+ return null;
5732
+ }
5733
+ }
5734
+ function sameLines(text, rules, file, lineOffset) {
5735
+ let source;
5736
+ try {
5737
+ source = fs9.readFileSync(file, "utf-8");
5738
+ } catch {
5739
+ return false;
5740
+ }
5741
+ const fileLines = source.split(/\r?\n/);
5742
+ const sheetLineStarts = [0];
5743
+ for (let i = 0; i < text.length; i++) if (text[i] === "\n") sheetLineStarts.push(i + 1);
5744
+ let low = 0;
5745
+ for (const rule of rules) {
5746
+ while (low + 1 < sheetLineStarts.length && sheetLineStarts[low + 1] <= rule.start) low++;
5747
+ const head = text.slice(rule.start, rule.open).split("\n")[0].trim();
5748
+ if (head && !(fileLines[low + lineOffset] ?? "").includes(head)) return false;
5749
+ }
5750
+ return true;
5751
+ }
5752
+ async function resolveSheet(sheet, text, dirs, fetchMap) {
5753
+ const lookup = await sheetMap(sheet, text, fetchMap);
5754
+ const rules = parseCssRules(text);
5755
+ if (lookup) {
5756
+ const sources = lookup.sources.map((source) => {
5757
+ const direct = sourceFile(source, dirs);
5758
+ if (direct) return direct;
5759
+ try {
5760
+ return /^[a-z][\w+.-]*:/i.test(source) ? null : fileForStyleUrl(new URL(source, sheet.sourceURL).href, dirs);
5761
+ } catch {
5762
+ return null;
5763
+ }
5764
+ });
5765
+ return { rules, lookup, file: null, lineOffset: 0, sources, exact: true };
5766
+ }
5767
+ if (sheet.devId) {
5768
+ const file2 = sourceFile(sheet.devId, dirs);
5769
+ if (!file2) return null;
5770
+ const index = /[?&]index=(\d+)/.exec(sheet.devId);
5771
+ const lineOffset2 = /\.(?:vue|svelte|astro)$/i.test(file2) && index ? styleBlockLine(file2, Number(index[1])) : 0;
5772
+ return { rules, lookup: null, file: file2, lineOffset: lineOffset2, sources: [], exact: sameLines(text, rules, file2, lineOffset2) };
5773
+ }
5774
+ const file = fileForStyleUrl(sheet.sourceURL, dirs);
5775
+ if (!file) return null;
5776
+ const lineOffset = sheet.isInline ? sheet.startLine : 0;
5777
+ return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
5778
+ }
5779
+ function cachedResolution(sheet, text, dirs, fetchMap) {
5780
+ const version = crypto3.createHash("sha1").update(text).digest("hex");
5781
+ const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
5782
+ " "
5783
+ );
5784
+ let pending = resolutions.get(key);
5785
+ if (!pending) {
5786
+ pending = resolveSheet(sheet, text, dirs, fetchMap).catch(() => null);
5787
+ resolutions.set(key, pending);
5788
+ if (resolutions.size > MAX_CACHED_SHEETS) resolutions.delete(resolutions.keys().next().value);
5789
+ }
5790
+ return pending;
5791
+ }
5792
+ function add(into, file, ranges, repoRoot, fileLevel = false) {
5793
+ if (ranges.length === 0 && !fileLevel) return;
5794
+ const key = relativeToRepo(file, repoRoot);
5795
+ into.set(key, [...into.get(key) ?? [], ...ranges]);
5796
+ }
5797
+ async function resolveCssCoverage(sheets, roots, fetchMap) {
5798
+ const reached = /* @__PURE__ */ new Map();
5799
+ const known = /* @__PURE__ */ new Map();
5800
+ for (const sheet of sheets) {
5801
+ if (sheet.text === void 0) continue;
5802
+ const dirs = scriptRoots(roots, sheet.sourceURL, sheet.pageUrl);
5803
+ const resolution = await cachedResolution(sheet, sheet.text, dirs, fetchMap);
5804
+ if (!resolution) continue;
5805
+ const used = usedCssRules(resolution.rules, [...sheet.used.values()]);
5806
+ if (!resolution.exact) {
5807
+ if (resolution.file && used.length > 0) add(reached, resolution.file, [], roots.repoRoot, true);
5808
+ continue;
5809
+ }
5810
+ const options = { lookup: resolution.lookup, lineOffset: resolution.lineOffset };
5811
+ const fileOf = (source) => source === -1 ? resolution.file : resolution.sources[source] ?? null;
5812
+ const matched = cssRuleLines(sheet.text, used, options);
5813
+ for (const [source, ranges] of matched) {
5814
+ const file = fileOf(source);
5815
+ if (file) add(reached, file, ranges, roots.repoRoot);
5816
+ }
5817
+ for (const [source, ranges] of cssRuleLines(sheet.text, allCssRules(resolution.rules), options)) {
5818
+ const file = fileOf(source);
5819
+ if (file) add(known, file, ranges, roots.repoRoot);
5820
+ }
5821
+ }
5822
+ return { reached, known };
5823
+ }
5824
+
5478
5825
  // src/internal/capture/resource-ledger.ts
5479
- var fs10 = __toESM(require("fs"));
5826
+ var fs11 = __toESM(require("fs"));
5480
5827
  var path13 = __toESM(require("path"));
5481
5828
  var import_node_module2 = require("module");
5482
5829
 
@@ -5484,12 +5831,12 @@ var import_node_module2 = require("module");
5484
5831
  var import_node_perf_hooks = require("perf_hooks");
5485
5832
 
5486
5833
  // src/internal/support/system-readers.ts
5487
- var fs9 = __toESM(require("fs"));
5834
+ var fs10 = __toESM(require("fs"));
5488
5835
  var path12 = __toESM(require("path"));
5489
5836
  var CLK_TCK = 100;
5490
5837
  function readText(file) {
5491
5838
  try {
5492
- return fs9.readFileSync(file, "utf8");
5839
+ return fs10.readFileSync(file, "utf8");
5493
5840
  } catch {
5494
5841
  return null;
5495
5842
  }
@@ -5571,14 +5918,14 @@ var ProcFs = class {
5571
5918
  }
5572
5919
  /** Whether this looks like a Linux `/proc` at all. */
5573
5920
  available() {
5574
- return fs9.existsSync(path12.join(this.root, "stat"));
5921
+ return fs10.existsSync(path12.join(this.root, "stat"));
5575
5922
  }
5576
5923
  /** Every process, keyed by pid. */
5577
5924
  table() {
5578
5925
  const out = /* @__PURE__ */ new Map();
5579
5926
  let names;
5580
5927
  try {
5581
- names = fs9.readdirSync(this.root);
5928
+ names = fs10.readdirSync(this.root);
5582
5929
  } catch {
5583
5930
  return out;
5584
5931
  }
@@ -5609,7 +5956,7 @@ var ProcFs = class {
5609
5956
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
5610
5957
  async pssKb(pid) {
5611
5958
  try {
5612
- return parseKbField(await fs9.promises.readFile(path12.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
5959
+ return parseKbField(await fs10.promises.readFile(path12.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
5613
5960
  } catch {
5614
5961
  return null;
5615
5962
  }
@@ -5617,7 +5964,7 @@ var ProcFs = class {
5617
5964
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
5618
5965
  resetPeakRss(pid) {
5619
5966
  try {
5620
- fs9.writeFileSync(path12.join(this.root, String(pid), "clear_refs"), "5");
5967
+ fs10.writeFileSync(path12.join(this.root, String(pid), "clear_refs"), "5");
5621
5968
  return true;
5622
5969
  } catch {
5623
5970
  return false;
@@ -5639,7 +5986,7 @@ var ProcFs = class {
5639
5986
  /** Open file descriptors of a process. */
5640
5987
  fdCount(pid) {
5641
5988
  try {
5642
- return fs9.readdirSync(path12.join(this.root, String(pid), "fd")).length;
5989
+ return fs10.readdirSync(path12.join(this.root, String(pid), "fd")).length;
5643
5990
  } catch {
5644
5991
  return null;
5645
5992
  }
@@ -6018,7 +6365,7 @@ var ResourceLedger = class {
6018
6365
  const census = this.census(null);
6019
6366
  if (this.resultsFile) {
6020
6367
  try {
6021
- fs10.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
6368
+ fs11.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
6022
6369
  `);
6023
6370
  } catch {
6024
6371
  }
@@ -6643,16 +6990,19 @@ function createSink() {
6643
6990
  onReleasedRequestsEnded: null,
6644
6991
  releasedQuietSince: 0,
6645
6992
  locatorPages: new LocatorPageLog(),
6646
- codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
6993
+ codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
6647
6994
  serverCoverage: /* @__PURE__ */ new Map(),
6648
6995
  baseURL: null
6649
6996
  };
6650
6997
  }
6651
6998
  var MAX_CODE_REACH_PAGES = 16;
6652
6999
  var CODE_REACH_START_WAIT_MS = 2e3;
7000
+ function pageCoverageInWorker() {
7001
+ return browserCoverageInWorker() || cssCoverageInWorker();
7002
+ }
6653
7003
  function maybeStartCodeReach(page, alreadyOpen = false) {
6654
7004
  const sink = currentSink;
6655
- if (!sink || !browserCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
7005
+ if (!sink || !pageCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
6656
7006
  const known = sink.codeReach.sessions.get(page);
6657
7007
  if (known) return known.started;
6658
7008
  if (alreadyOpen) {
@@ -6665,7 +7015,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
6665
7015
  return started;
6666
7016
  }
6667
7017
  async function startCodeReachOnOpenPages() {
6668
- if (!browserCoverageInWorker() || OPEN_PAGES.size === 0) return;
7018
+ if (!pageCoverageInWorker() || OPEN_PAGES.size === 0) return;
6669
7019
  const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
6670
7020
  await Promise.race([started, new Promise((resolve5) => setTimeout(resolve5, CODE_REACH_START_WAIT_MS).unref())]);
6671
7021
  }
@@ -6708,22 +7058,28 @@ async function stopSinkCodeReach(sink, closing) {
6708
7058
  async function stopPageCodeReach(sink, page, session, roots) {
6709
7059
  try {
6710
7060
  if (!await session.started || isPageClosed(page)) return;
6711
- const entries = await stopCodeReach(page);
6712
- if (!entries) return;
6713
- const { reached, known } = await resolveCodeReachFunctions(entries, roots, pageMapFetcher(page), page.url(), {
7061
+ const { scripts, sheets } = await stopPageCoverage(page);
7062
+ const fetchMap = pageMapFetcher(page);
7063
+ const append = (into, from) => {
7064
+ for (const [file, ranges] of from) into.set(file, [...into.get(file) ?? [], ...ranges]);
7065
+ };
7066
+ if (sheets.length > 0) {
7067
+ const rules = await resolveCssCoverage(sheets, roots, fetchMap);
7068
+ append(sink.codeReach.cssFiles, rules.reached);
7069
+ append(sink.codeReach.cssKnown, rules.known);
7070
+ }
7071
+ if (!scripts) return;
7072
+ const { reached, known } = await resolveCodeReachFunctions(scripts, roots, fetchMap, page.url(), {
6714
7073
  known: browserFunctionsInWorker()
6715
7074
  });
6716
- for (const [file, functions] of reached) {
6717
- sink.codeReach.files.set(file, [...sink.codeReach.files.get(file) ?? [], ...functions]);
6718
- }
6719
- for (const [file, functions] of known) {
6720
- sink.codeReach.known.set(file, [...sink.codeReach.known.get(file) ?? [], ...functions]);
6721
- }
7075
+ append(sink.codeReach.files, reached);
7076
+ append(sink.codeReach.known, known);
6722
7077
  } catch {
6723
7078
  }
6724
7079
  }
6725
7080
  var currentSink = null;
6726
7081
  var KNOWN_FUNCTIONS_SENT = /* @__PURE__ */ new Map();
7082
+ var KNOWN_CSS_RULES_SENT = /* @__PURE__ */ new Map();
6727
7083
  var PENDING_PROBES = /* @__PURE__ */ new Set();
6728
7084
  async function drainPendingProbes(capMs) {
6729
7085
  if (PENDING_PROBES.size === 0) return;
@@ -7879,6 +8235,21 @@ async function flushSink(sink, testInfo) {
7879
8235
  body: Buffer.from(JSON.stringify(part))
7880
8236
  });
7881
8237
  }
8238
+ if (cssCoverageInWorker()) {
8239
+ const rules = sink.codeReach.cssFiles.size > 0 ? browserFunctionCoverage(sink.codeReach.cssFiles) : null;
8240
+ if (rules) {
8241
+ await testInfo.attach(ATTACHMENT_NAMES.cssCoverage, {
8242
+ contentType: "application/json",
8243
+ body: Buffer.from(JSON.stringify(rules))
8244
+ });
8245
+ }
8246
+ for (const part of newKnownFunctions(sink.codeReach.cssKnown, KNOWN_CSS_RULES_SENT)) {
8247
+ await testInfo.attach(ATTACHMENT_NAMES.cssRules, {
8248
+ contentType: "application/json",
8249
+ body: Buffer.from(JSON.stringify(part))
8250
+ });
8251
+ }
8252
+ }
7882
8253
  if (sink.serverCoverage.size > 0) {
7883
8254
  const requests = {};
7884
8255
  for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];