tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_resources_stylesheets.js (21269B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 // Test the ResourceCommand API around STYLESHEET.
      7 
      8 const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
      9 
     10 const STYLE_TEST_URL = URL_ROOT_SSL + "style_document.html";
     11 
     12 const EXISTING_RESOURCES = [
     13  {
     14    styleText: "body { color: lime; }",
     15    href: null,
     16    nodeHref:
     17      "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
     18    isNew: false,
     19    disabled: false,
     20    constructed: false,
     21    ruleCount: 1,
     22    atRules: [],
     23  },
     24  {
     25    styleText: "body { margin: 1px; }",
     26    href: "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.css",
     27    nodeHref:
     28      "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
     29    isNew: false,
     30    disabled: false,
     31    constructed: false,
     32    ruleCount: 1,
     33    atRules: [],
     34  },
     35  {
     36    styleText: "",
     37    href: null,
     38    nodeHref: null,
     39    isNew: false,
     40    disabled: false,
     41    constructed: true,
     42    ruleCount: 1,
     43    atRules: [],
     44  },
     45  {
     46    styleText: "body { background-color: pink; }",
     47    href: null,
     48    nodeHref:
     49      "https://example.org/browser/devtools/shared/commands/resource/tests/style_iframe.html",
     50    isNew: false,
     51    disabled: false,
     52    constructed: false,
     53    ruleCount: 1,
     54    atRules: [],
     55  },
     56  {
     57    styleText: "body { padding: 1px; }",
     58    href: "https://example.org/browser/devtools/shared/commands/resource/tests/style_iframe.css",
     59    nodeHref:
     60      "https://example.org/browser/devtools/shared/commands/resource/tests/style_iframe.html",
     61    isNew: false,
     62    disabled: false,
     63    constructed: false,
     64    ruleCount: 1,
     65    atRules: [],
     66  },
     67 ];
     68 
     69 const ADDITIONAL_INLINE_RESOURCE = {
     70  styleText:
     71    "@media all { body { color: red; } } @media print { body { color: cyan; } } body { font-size: 10px; }",
     72  href: null,
     73  nodeHref:
     74    "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
     75  isNew: false,
     76  disabled: false,
     77  constructed: false,
     78  ruleCount: 5,
     79  atRules: [
     80    {
     81      type: "media",
     82      conditionText: "all",
     83      matches: true,
     84      line: 1,
     85      column: 1,
     86    },
     87    {
     88      type: "media",
     89      conditionText: "print",
     90      matches: false,
     91      line: 1,
     92      column: 37,
     93    },
     94  ],
     95 };
     96 
     97 const ADDITIONAL_CONSTRUCTED_RESOURCE = {
     98  styleText: "",
     99  href: null,
    100  nodeHref: null,
    101  isNew: false,
    102  disabled: false,
    103  constructed: true,
    104  ruleCount: 2,
    105  atRules: [],
    106 };
    107 
    108 const ADDITIONAL_FROM_ACTOR_RESOURCE = {
    109  styleText: "body { font-size: 10px; }",
    110  href: null,
    111  nodeHref:
    112    "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
    113  isNew: true,
    114  disabled: false,
    115  constructed: false,
    116  ruleCount: 1,
    117  atRules: [],
    118 };
    119 
    120 add_task(async function () {
    121  // Enable @property
    122  await pushPref("layout.css.properties-and-values.enabled", true);
    123  await testResourceAvailableDestroyedFeature();
    124  await testResourceUpdateFeature();
    125  await testNestedResourceUpdateFeature();
    126 });
    127 
    128 function pushAvailableResource(availableResources) {
    129  // TODO(bug 1826538): Find a better way of dealing with these.
    130  return function (resources) {
    131    for (const resource of resources) {
    132      if (resource.href?.startsWith("resource://")) {
    133        continue;
    134      }
    135      availableResources.push(resource);
    136    }
    137  };
    138 }
    139 
    140 async function testResourceAvailableDestroyedFeature() {
    141  info("Check resource available feature of the ResourceCommand");
    142 
    143  const tab = await addTab(STYLE_TEST_URL);
    144  let resourceTimingEntryCounts = await getResourceTimingCount(tab);
    145  is(
    146    resourceTimingEntryCounts,
    147    2,
    148    "Should have two entires for resource timing"
    149  );
    150 
    151  const { client, resourceCommand, targetCommand } =
    152    await initResourceCommand(tab);
    153 
    154  info("Check whether ResourceCommand gets existing stylesheet");
    155  const availableResources = [];
    156  const destroyedResources = [];
    157  await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
    158    onAvailable: pushAvailableResource(availableResources),
    159    onDestroyed: resources => destroyedResources.push(...resources),
    160  });
    161 
    162  is(
    163    availableResources.length,
    164    EXISTING_RESOURCES.length,
    165    "Length of existing resources is correct"
    166  );
    167  for (let i = 0; i < availableResources.length; i++) {
    168    const availableResource = availableResources[i];
    169    // We can not expect the resources to always be forwarded in the same order.
    170    // See intermittent Bug 1655016.
    171    const expectedResource = findMatchingExpectedResource(availableResource);
    172    ok(expectedResource, "Found a matching expected resource for the resource");
    173    await assertResource(availableResource, expectedResource);
    174  }
    175 
    176  resourceTimingEntryCounts = await getResourceTimingCount(tab);
    177  is(
    178    resourceTimingEntryCounts,
    179    2,
    180    "Should still have two entires for resource timing after devtools APIs have been triggered"
    181  );
    182 
    183  info("Check whether ResourceCommand gets additonal stylesheet");
    184  await ContentTask.spawn(
    185    tab.linkedBrowser,
    186    ADDITIONAL_INLINE_RESOURCE.styleText,
    187    text => {
    188      const document = content.document;
    189      const stylesheet = document.createElement("style");
    190      stylesheet.id = "inline-from-test";
    191      stylesheet.textContent = text;
    192      document.body.appendChild(stylesheet);
    193    }
    194  );
    195  await waitUntil(
    196    () => availableResources.length === EXISTING_RESOURCES.length + 1
    197  );
    198  await assertResource(
    199    availableResources[availableResources.length - 1],
    200    ADDITIONAL_INLINE_RESOURCE
    201  );
    202 
    203  info("Check whether ResourceCommand gets additonal constructed stylesheet");
    204  await ContentTask.spawn(tab.linkedBrowser, null, () => {
    205    const document = content.document;
    206    const s = new content.CSSStyleSheet();
    207    // We use the different number of rules to meaningfully differentiate
    208    // between constructed stylesheets.
    209    s.replaceSync("foo { color: red } bar { color: blue }");
    210    // TODO(bug 1751346): wrappedJSObject should be unnecessary.
    211    document.wrappedJSObject.adoptedStyleSheets.push(s);
    212  });
    213  await waitUntil(
    214    () => availableResources.length === EXISTING_RESOURCES.length + 2
    215  );
    216  await assertResource(
    217    availableResources[availableResources.length - 1],
    218    ADDITIONAL_CONSTRUCTED_RESOURCE
    219  );
    220 
    221  info(
    222    "Check whether ResourceCommand gets additonal stylesheet which is added by DevTools"
    223  );
    224  const styleSheetsFront =
    225    await targetCommand.targetFront.getFront("stylesheets");
    226  await styleSheetsFront.addStyleSheet(
    227    ADDITIONAL_FROM_ACTOR_RESOURCE.styleText
    228  );
    229  await waitUntil(
    230    () => availableResources.length === EXISTING_RESOURCES.length + 3
    231  );
    232  await assertResource(
    233    availableResources[availableResources.length - 1],
    234    ADDITIONAL_FROM_ACTOR_RESOURCE
    235  );
    236 
    237  info("Check resource destroyed feature of the ResourceCommand");
    238  is(destroyedResources.length, 0, "There was no removed stylesheets yet");
    239 
    240  info("Remove inline stylesheet added in the test");
    241  await ContentTask.spawn(tab.linkedBrowser, null, () => {
    242    content.document.querySelector("#inline-from-test").remove();
    243  });
    244  await waitUntil(() => destroyedResources.length === 1);
    245  assertDestroyed(destroyedResources[0], {
    246    resourceId: availableResources.at(-3).resourceId,
    247  });
    248 
    249  info("Remove existing top-level inline stylesheet");
    250  await ContentTask.spawn(tab.linkedBrowser, null, () => {
    251    content.document.querySelector("style").remove();
    252  });
    253  await waitUntil(() => destroyedResources.length === 2);
    254  assertDestroyed(destroyedResources[1], {
    255    resourceId: availableResources.find(
    256      resource =>
    257        findMatchingExpectedResource(resource) === EXISTING_RESOURCES[0]
    258    ).resourceId,
    259  });
    260 
    261  info("Remove existing top-level <link> stylesheet");
    262  await ContentTask.spawn(tab.linkedBrowser, null, () => {
    263    content.document.querySelector("link").remove();
    264  });
    265  await waitUntil(() => destroyedResources.length === 3);
    266  assertDestroyed(destroyedResources[2], {
    267    resourceId: availableResources.find(
    268      resource =>
    269        findMatchingExpectedResource(resource) === EXISTING_RESOURCES[1]
    270    ).resourceId,
    271  });
    272 
    273  info("Remove existing iframe inline stylesheet");
    274  const iframeBrowsingContext = await SpecialPowers.spawn(
    275    tab.linkedBrowser,
    276    [],
    277    () => content.document.querySelector("iframe").browsingContext
    278  );
    279 
    280  await SpecialPowers.spawn(iframeBrowsingContext, [], () => {
    281    content.document.querySelector("style").remove();
    282  });
    283  await waitUntil(() => destroyedResources.length === 4);
    284  assertDestroyed(destroyedResources[3], {
    285    resourceId: availableResources.find(
    286      resource =>
    287        findMatchingExpectedResource(resource) === EXISTING_RESOURCES[3]
    288    ).resourceId,
    289  });
    290 
    291  info("Remove existing iframe <link> stylesheet");
    292  await SpecialPowers.spawn(iframeBrowsingContext, [], () => {
    293    content.document.querySelector("link").remove();
    294  });
    295  await waitUntil(() => destroyedResources.length === 5);
    296  assertDestroyed(destroyedResources[4], {
    297    resourceId: availableResources.find(
    298      resource =>
    299        findMatchingExpectedResource(resource) === EXISTING_RESOURCES[4]
    300    ).resourceId,
    301  });
    302 
    303  targetCommand.destroy();
    304  await client.close();
    305 }
    306 
    307 async function testResourceUpdateFeature() {
    308  info("Check resource update feature of the ResourceCommand");
    309 
    310  const tab = await addTab(STYLE_TEST_URL);
    311 
    312  const { client, resourceCommand, targetCommand } =
    313    await initResourceCommand(tab);
    314 
    315  info("Setup the watcher");
    316  const availableResources = [];
    317  const updates = [];
    318  await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
    319    onAvailable: pushAvailableResource(availableResources),
    320    onUpdated: newUpdates => updates.push(...newUpdates),
    321  });
    322  is(
    323    availableResources.length,
    324    EXISTING_RESOURCES.length,
    325    "Length of existing resources is correct"
    326  );
    327  is(updates.length, 0, "there's no update yet");
    328 
    329  info("Check toggleDisabled function");
    330  // Retrieve the stylesheet of the top-level target
    331  const resource = availableResources.find(
    332    innerResource => innerResource.targetFront.isTopLevel
    333  );
    334  const styleSheetsFront = await resource.targetFront.getFront("stylesheets");
    335  await styleSheetsFront.toggleDisabled(resource.resourceId);
    336  await waitUntil(() => updates.length === 1);
    337 
    338  // Check the content of the update object.
    339  assertUpdate(updates[0].update, {
    340    resourceId: resource.resourceId,
    341    updateType: "property-change",
    342  });
    343  is(
    344    updates[0].update.resourceUpdates.disabled,
    345    true,
    346    "resourceUpdates is correct"
    347  );
    348 
    349  // Check whether the cached resource is updated correctly.
    350  is(
    351    updates[0].resource.disabled,
    352    true,
    353    "cached resource is updated correctly"
    354  );
    355 
    356  // Check whether the actual stylesheet is updated correctly.
    357  const styleSheetDisabled = await ContentTask.spawn(
    358    tab.linkedBrowser,
    359    null,
    360    () => {
    361      const document = content.document;
    362      const stylesheet = document.styleSheets[0];
    363      return stylesheet.disabled;
    364    }
    365  );
    366  is(styleSheetDisabled, true, "actual stylesheet was updated correctly");
    367 
    368  info("Check update function");
    369  const expectedAtRules = [
    370    {
    371      type: "media",
    372      conditionText: "screen",
    373      matches: true,
    374    },
    375    {
    376      type: "media",
    377      conditionText: "print",
    378      matches: false,
    379    },
    380  ];
    381 
    382  const updateCause = "updated-by-test";
    383  await styleSheetsFront.update(
    384    resource.resourceId,
    385    "@media screen { color: red; } @media print { color: green; } body { color: cyan; }",
    386    false,
    387    updateCause
    388  );
    389  await waitUntil(() => updates.length === 4);
    390 
    391  assertUpdate(updates[1].update, {
    392    resourceId: resource.resourceId,
    393    updateType: "property-change",
    394  });
    395  is(
    396    updates[1].update.resourceUpdates.ruleCount,
    397    3,
    398    "resourceUpdates is correct"
    399  );
    400  is(updates[1].resource.ruleCount, 3, "cached resource is updated correctly");
    401 
    402  assertUpdate(updates[2].update, {
    403    resourceId: resource.resourceId,
    404    updateType: "style-applied",
    405    event: {
    406      cause: updateCause,
    407    },
    408  });
    409  is(
    410    updates[2].update.resourceUpdates,
    411    undefined,
    412    "resourceUpdates is correct"
    413  );
    414 
    415  assertUpdate(updates[3].update, {
    416    resourceId: resource.resourceId,
    417    updateType: "at-rules-changed",
    418  });
    419  assertAtRules(updates[3].update.resourceUpdates.atRules, expectedAtRules);
    420 
    421  // Check the actual page.
    422  const styleSheetResult = await getStyleSheetResult(tab);
    423 
    424  is(
    425    styleSheetResult.ruleCount,
    426    3,
    427    "ruleCount of actual stylesheet is updated correctly"
    428  );
    429  assertAtRules(styleSheetResult.atRules, expectedAtRules);
    430 
    431  targetCommand.destroy();
    432  await client.close();
    433 }
    434 
    435 function resizeToInner(win, wantedInnerWidth, wantedInnerHeight) {
    436  const diffX = wantedInnerWidth - win.innerWidth;
    437  const diffY = wantedInnerHeight - win.innerHeight;
    438  win.resizeBy(diffX, diffY);
    439 }
    440 
    441 async function testNestedResourceUpdateFeature() {
    442  info("Check nested resource update feature of the ResourceCommand");
    443 
    444  const tab = await addTab(STYLE_TEST_URL);
    445  const win = tab.ownerGlobal;
    446 
    447  const { innerWidth: originalWindowWidth, innerHeight: originalWindowHeight } =
    448    win;
    449 
    450  registerCleanupFunction(() => {
    451    resizeToInner(win, originalWindowWidth, originalWindowHeight);
    452  });
    453 
    454  const { client, resourceCommand, targetCommand } =
    455    await initResourceCommand(tab);
    456 
    457  info("Setup the watcher");
    458  const availableResources = [];
    459  const updates = [];
    460  await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
    461    onAvailable: pushAvailableResource(availableResources),
    462    onUpdated: newUpdates => updates.push(...newUpdates),
    463  });
    464  is(
    465    availableResources.length,
    466    EXISTING_RESOURCES.length,
    467    "Length of existing resources is correct"
    468  );
    469 
    470  info("Apply new media query");
    471  // In order to avoid applying the media query (min-height: 400px).
    472  if (originalWindowHeight !== 300) {
    473    await new Promise(resolve => {
    474      win.addEventListener("resize", resolve, { once: true });
    475      resizeToInner(win, originalWindowWidth, 300);
    476    });
    477  }
    478 
    479  // Retrieve the stylesheet of the top-level target
    480  const resource = availableResources.find(
    481    innerResource => innerResource.targetFront.isTopLevel
    482  );
    483  const styleSheetsFront = await resource.targetFront.getFront("stylesheets");
    484  await styleSheetsFront.update(
    485    resource.resourceId,
    486    `@media (min-height: 400px) {
    487      html {
    488        color: red;
    489      }
    490      @layer myLayer {
    491        @supports (container-type) {
    492          :root {
    493            color: gold;
    494            container: root inline-size;
    495          }
    496 
    497          @container root (width > 10px) {
    498            body {
    499              color: gold;
    500            }
    501          }
    502        }
    503      }
    504    }
    505    @property --my-property {
    506      syntax: "<color>";
    507      inherits: true;
    508      initial-value: #f06;
    509    }`,
    510    false
    511  );
    512  await waitUntil(() => updates.length === 3);
    513  is(
    514    updates.at(-1).resource.ruleCount,
    515    8,
    516    "Resource in update has expected ruleCount"
    517  );
    518 
    519  is(resource.atRules[0].matches, false, "Media query is not matched yet");
    520 
    521  info("Change window size to fire matches-change event");
    522  resizeToInner(win, originalWindowWidth, 500);
    523  await waitUntil(() => updates.length === 4);
    524 
    525  // Check the update content.
    526  const targetUpdate = updates[3];
    527  assertUpdate(targetUpdate.update, {
    528    resourceId: resource.resourceId,
    529    updateType: "matches-change",
    530  });
    531  Assert.strictEqual(
    532    resource,
    533    targetUpdate.resource,
    534    "Update object has the same resource"
    535  );
    536 
    537  is(
    538    JSON.stringify(targetUpdate.update.nestedResourceUpdates[0].path),
    539    JSON.stringify(["atRules", 0, "matches"]),
    540    "path of nestedResourceUpdates is correct"
    541  );
    542  is(
    543    targetUpdate.update.nestedResourceUpdates[0].value,
    544    true,
    545    "value of nestedResourceUpdates is correct"
    546  );
    547 
    548  // Check the resource.
    549  const expectedAtRules = [
    550    {
    551      type: "media",
    552      conditionText: "(min-height: 400px)",
    553      matches: true,
    554    },
    555    {
    556      type: "layer",
    557      layerName: "myLayer",
    558    },
    559    {
    560      type: "support",
    561      conditionText: "(container-type)",
    562    },
    563    {
    564      type: "container",
    565      conditionText: "root (width > 10px)",
    566    },
    567    {
    568      type: "property",
    569      propertyName: "--my-property",
    570    },
    571  ];
    572 
    573  assertAtRules(targetUpdate.resource.atRules, expectedAtRules);
    574 
    575  // Check the actual page.
    576  const styleSheetResult = await getStyleSheetResult(tab);
    577  is(
    578    styleSheetResult.ruleCount,
    579    8,
    580    "ruleCount of actual stylesheet is updated correctly"
    581  );
    582  assertAtRules(styleSheetResult.atRules, expectedAtRules);
    583 
    584  resizeToInner(win, originalWindowWidth, originalWindowHeight);
    585 
    586  targetCommand.destroy();
    587  await client.close();
    588 }
    589 
    590 function findMatchingExpectedResource(resource) {
    591  return EXISTING_RESOURCES.find(
    592    expected =>
    593      resource.href === expected.href &&
    594      resource.nodeHref === expected.nodeHref &&
    595      resource.ruleCount === expected.ruleCount &&
    596      resource.constructed == expected.constructed
    597  );
    598 }
    599 
    600 async function getStyleSheetResult(tab) {
    601  const result = await ContentTask.spawn(tab.linkedBrowser, null, () => {
    602    const document = content.document;
    603    const stylesheet = document.styleSheets[0];
    604    let ruleCount = 0;
    605    const atRules = [];
    606 
    607    const traverseRules = ruleList => {
    608      for (const rule of ruleList) {
    609        ruleCount++;
    610 
    611        if (rule.media) {
    612          let matches = false;
    613          try {
    614            const mql = content.matchMedia(rule.media.mediaText);
    615            matches = mql.matches;
    616          } catch (e) {
    617            // Ignored
    618          }
    619 
    620          atRules.push({
    621            type: "media",
    622            conditionText: rule.conditionText,
    623            matches,
    624          });
    625        } else if (rule instanceof content.CSSContainerRule) {
    626          atRules.push({
    627            type: "container",
    628            conditionText: rule.conditionText,
    629          });
    630        } else if (rule instanceof content.CSSLayerBlockRule) {
    631          atRules.push({ type: "layer", layerName: rule.name });
    632        } else if (rule instanceof content.CSSSupportsRule) {
    633          atRules.push({
    634            type: "support",
    635            conditionText: rule.conditionText,
    636          });
    637        } else if (rule instanceof content.CSSPropertyRule) {
    638          atRules.push({
    639            type: "property",
    640            propertyName: rule.name,
    641          });
    642        }
    643 
    644        if (rule.cssRules) {
    645          traverseRules(rule.cssRules);
    646        }
    647      }
    648    };
    649    traverseRules(stylesheet.cssRules);
    650 
    651    return { ruleCount, atRules };
    652  });
    653 
    654  return result;
    655 }
    656 
    657 function assertAtRules(atRules, expectedAtRules) {
    658  is(
    659    atRules.length,
    660    expectedAtRules.length,
    661    "Length of the atRules is correct"
    662  );
    663 
    664  for (let i = 0; i < atRules.length; i++) {
    665    const atRule = atRules[i];
    666    const expected = expectedAtRules[i];
    667    is(atRule.type, expected.type, "at-rule is of expected type");
    668    is(
    669      atRules[i].conditionText,
    670      expected.conditionText,
    671      "conditionText is correct"
    672    );
    673    if (expected.type === "media") {
    674      is(atRule.matches, expected.matches, "matches is correct");
    675    } else if (expected.type === "layer") {
    676      is(atRule.layerName, expected.layerName, "layerName is correct");
    677    } else if (expected.type === "property") {
    678      is(atRule.propertyName, expected.propertyName, "propertyName is correct");
    679    }
    680 
    681    if (expected.line !== undefined) {
    682      is(atRule.line, expected.line, "line is correct");
    683    }
    684 
    685    if (expected.column !== undefined) {
    686      is(atRule.column, expected.column, "column is correct");
    687    }
    688  }
    689 }
    690 
    691 async function assertResource(resource, expected) {
    692  is(
    693    resource.resourceType,
    694    ResourceCommand.TYPES.STYLESHEET,
    695    "Resource type is correct"
    696  );
    697  const styleText = (await getStyleSheetResourceText(resource)).trim();
    698  is(styleText, expected.styleText, "Style text is correct");
    699  is(resource.href, expected.href, "href is correct");
    700  is(resource.nodeHref, expected.nodeHref, "nodeHref is correct");
    701  is(resource.isNew, expected.isNew, "isNew is correct");
    702  is(resource.disabled, expected.disabled, "disabled is correct");
    703  is(resource.constructed, expected.constructed, "constructed is correct");
    704  is(resource.ruleCount, expected.ruleCount, "ruleCount is correct");
    705  assertAtRules(resource.atRules, expected.atRules);
    706 }
    707 
    708 function assertUpdate(update, expected) {
    709  is(
    710    update.resourceType,
    711    ResourceCommand.TYPES.STYLESHEET,
    712    "Resource type is correct"
    713  );
    714  is(update.resourceId, expected.resourceId, "resourceId is correct");
    715  is(update.updateType, expected.updateType, "updateType is correct");
    716  if (expected.event?.cause) {
    717    is(update.event?.cause, expected.event.cause, "cause is correct");
    718  }
    719 }
    720 
    721 function assertDestroyed(resource, expected) {
    722  is(
    723    resource.resourceType,
    724    ResourceCommand.TYPES.STYLESHEET,
    725    "Resource type is correct"
    726  );
    727  is(resource.resourceId, expected.resourceId, "resourceId is correct");
    728 }
    729 
    730 function getResourceTimingCount(tab) {
    731  return ContentTask.spawn(tab.linkedBrowser, [], () => {
    732    return content.performance.getEntriesByType("resource").length;
    733  });
    734 }