Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/toolkit/mozapps/extensions/test/browser/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 48 kB image not shown  

Quelle  head.js   Sprache: JAVA

 
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

/* globals end_test */

/* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */

const { TelemetryTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TelemetryTestUtils.sys.mjs"
);

let { AddonManagerPrivate } = ChromeUtils.importESModule(
  "resource://gre/modules/AddonManager.sys.mjs"
);

var pathParts = gTestPath.split("/");
// Drop the test filename
pathParts.splice(pathParts.length - 1, pathParts.length);

const RELATIVE_DIR = pathParts.slice(4).join("/") + "/";

const TESTROOT = "http://example.com/" + RELATIVE_DIR;
const SECURE_TESTROOT = "https://example.com/" + RELATIVE_DIR;
const TESTROOT2 = "http://example.org/" + RELATIVE_DIR;
const SECURE_TESTROOT2 = "https://example.org/" + RELATIVE_DIR;
const CHROMEROOT = pathParts.join("/") + "/";
const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
const PREF_XPI_ENABLED = "xpinstall.enabled";
const PREF_UPDATEURL = "extensions.update.url";
const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";

const MANAGER_URI = "about:addons";
const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
const PREF_STRICT_COMPAT = "extensions.strictCompatibility";

var PREF_CHECK_COMPATIBILITY;
(function () {
  var channel = Services.prefs.getCharPref("app.update.channel""default");
  if (
    channel != "aurora" &&
    channel != "beta" &&
    channel != "release" &&
    channel != "esr"
  ) {
    var version = "nightly";
  } else {
    version = Services.appinfo.version.replace(
      /^([^\.]+\.[0-9]+[a-z]*).*/gi,
      "$1"
    );
  }
  PREF_CHECK_COMPATIBILITY = "extensions.checkCompatibility." + version;
})();

var gPendingTests = [];
var gTestsRun = 0;
var gTestStart = null;

var gRestorePrefs = [
  { name: PREF_LOGGING_ENABLED },
  { name: "extensions.webservice.discoverURL" },
  { name: "extensions.update.url" },
  { name: "extensions.update.background.url" },
  { name: "extensions.update.enabled" },
  { name: "extensions.update.autoUpdateDefault" },
  { name: "extensions.getAddons.get.url" },
  { name: "extensions.getAddons.getWithPerformance.url" },
  { name: "extensions.getAddons.cache.enabled" },
  { name: "devtools.chrome.enabled" },
  { name: PREF_STRICT_COMPAT },
  { name: PREF_CHECK_COMPATIBILITY },
];

for (let pref of gRestorePrefs) {
  if (!Services.prefs.prefHasUserValue(pref.name)) {
    pref.type = "clear";
    continue;
  }
  pref.type = Services.prefs.getPrefType(pref.name);
  if (pref.type == Services.prefs.PREF_BOOL) {
    pref.value = Services.prefs.getBoolPref(pref.name);
  } else if (pref.type == Services.prefs.PREF_INT) {
    pref.value = Services.prefs.getIntPref(pref.name);
  } else if (pref.type == Services.prefs.PREF_STRING) {
    pref.value = Services.prefs.getCharPref(pref.name);
  }
}

// Turn logging on for all tests
Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);

function promiseFocus(window) {
  return new Promise(resolve => waitForFocus(resolve, window));
}

// Tools to disable and re-enable the background update and blocklist timers
// so that tests can protect themselves from unwanted timer events.
var gCatMan = Services.catMan;
// Default value from toolkit/mozapps/extensions/extensions.manifest, but disable*UpdateTimer()
// records the actual value so we can put it back in enable*UpdateTimer()
var backgroundUpdateConfig =
  "@mozilla.org/addons/integration;1,getService,addon-background-update-timer,extensions.update.interval,86400";

var UTIMER = "update-timer";
var AMANAGER = "addonManager";
var BLOCKLIST = "nsBlocklistService";

function disableBackgroundUpdateTimer() {
  info("Disabling " + UTIMER + " " + AMANAGER);
  backgroundUpdateConfig = gCatMan.getCategoryEntry(UTIMER, AMANAGER);
  gCatMan.deleteCategoryEntry(UTIMER, AMANAGER, true);
}

function enableBackgroundUpdateTimer() {
  info("Enabling " + UTIMER + " " + AMANAGER);
  gCatMan.addCategoryEntry(
    UTIMER,
    AMANAGER,
    backgroundUpdateConfig,
    false,
    true
  );
}

registerCleanupFunction(function () {
  // Restore prefs
  for (let pref of gRestorePrefs) {
    if (pref.type == "clear") {
      Services.prefs.clearUserPref(pref.name);
    } else if (pref.type == Services.prefs.PREF_BOOL) {
      Services.prefs.setBoolPref(pref.name, pref.value);
    } else if (pref.type == Services.prefs.PREF_INT) {
      Services.prefs.setIntPref(pref.name, pref.value);
    } else if (pref.type == Services.prefs.PREF_STRING) {
      Services.prefs.setCharPref(pref.name, pref.value);
    }
  }

  return AddonManager.getAllInstalls().then(aInstalls => {
    for (let install of aInstalls) {
      if (install instanceof MockInstall) {
        continue;
      }

      ok(
        false,
        "Should not have seen an install of " +
          install.sourceURI.spec +
          " in state " +
          install.state
      );
      install.cancel();
    }
  });
});

function log_exceptions(aCallback, ...aArgs) {
  try {
    return aCallback.apply(null, aArgs);
  } catch (e) {
    info("Exception thrown: " + e);
    throw e;
  }
}

function log_callback(aPromise, aCallback) {
  aPromise.then(aCallback).catch(e => info("Exception thrown: " + e));
  return aPromise;
}

function add_test(test) {
  gPendingTests.push(test);
}

function run_next_test() {
  // Make sure we're not calling run_next_test from inside an add_task() test
  // We're inside the browser_test.js 'testScope' here
  if (this.__tasks) {
    throw new Error(
      "run_next_test() called from an add_task() test function. " +
        "run_next_test() should not be called from inside add_task() " +
        "under any circumstances!"
    );
  }
  if (gTestsRun > 0) {
    info("Test " + gTestsRun + " took " + (Date.now() - gTestStart) + "ms");
  }

  if (!gPendingTests.length) {
    executeSoon(end_test);
    return;
  }

  gTestsRun++;
  var test = gPendingTests.shift();
  if (test.name) {
    info("Running test " + gTestsRun + " (" + test.name + ")");
  } else {
    info("Running test " + gTestsRun);
  }

  gTestStart = Date.now();
  executeSoon(() => log_exceptions(test));
}

var get_tooltip_info = async function (addonEl) {
  // Extract from title attribute.
  const { addon } = addonEl;
  const name = addon.name;

  let nameWithVersion = addonEl.addonNameEl.title;
  if (addonEl.addon.userDisabled) {
    // TODO - Bug 1558077: Currently Fluent is clearing the addon title
    // when the addon is disabled, fixing it requires changes to the
    // HTML about:addons localized strings, and then remove this
    // workaround.
    nameWithVersion = `${name} ${addon.version}`;
  }

  return {
    name,
    version: nameWithVersion.substring(name.length + 1),
  };
};

function get_addon_file_url(aFilename) {
  try {
    var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
      Ci.nsIChromeRegistry
    );
    var fileurl = cr.convertChromeURL(
      makeURI(CHROMEROOT + "addons/" + aFilename)
    );
    return fileurl.QueryInterface(Ci.nsIFileURL);
  } catch (ex) {
    var jar = getJar(CHROMEROOT + "addons/" + aFilename);
    var tmpDir = extractJarToTmp(jar);
    tmpDir.append(aFilename);

    return Services.io.newFileURI(tmpDir).QueryInterface(Ci.nsIFileURL);
  }
}

function check_all_in_list(aManager, aIds, aIgnoreExtras) {
  var doc = aManager.document;
  var list = doc.getElementById("addon-list");

  var inlist = [];
  var node = list.firstChild;
  while (node) {
    if (node.value) {
      inlist.push(node.value);
    }
    node = node.nextSibling;
  }

  for (let id of aIds) {
    if (!inlist.includes(id)) {
      ok(false"Should find " + id + " in the list");
    }
  }

  if (aIgnoreExtras) {
    return;
  }

  for (let inlistItem of inlist) {
    if (!aIds.includes(inlistItem)) {
      ok(false"Shouldn't have seen " + inlistItem + " in the list");
    }
  }
}

function getAddonCard(win, id) {
  return win.document.querySelector(`addon-card[addon-id="${id}"]`);
}

async function wait_for_view_load(
  aManagerWindow,
  aCallback,
  aForceWait,
  aLongerTimeout
) {
  // Wait one tick to make sure that the microtask related to an
  // async loadView call originated from outsite about:addons
  // is already executing (otherwise isLoading would be still false
  // and we wouldn't be waiting for that load before resolving
  // the promise returned by this test helper function).
  await Promise.resolve();

  let p = new Promise(resolve => {
    requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2);

    if (!aForceWait && !aManagerWindow.gViewController.isLoading) {
      resolve(aManagerWindow);
      return;
    }

    aManagerWindow.document.addEventListener(
      "view-loaded",
      function () {
        resolve(aManagerWindow);
      },
      { once: true }
    );
  });

  return log_callback(p, aCallback);
}

function wait_for_manager_load(aManagerWindow, aCallback) {
  info("Waiting for initialization");
  return log_callback(
    aManagerWindow.promiseInitialized.then(() => aManagerWindow),
    aCallback
  );
}

function open_manager(
  aView,
  aCallback,
  aLoadCallback,
  aLongerTimeout,
  aWin = window
) {
  let p = new Promise(resolve => {
    async function setup_manager(aManagerWindow) {
      if (aLoadCallback) {
        log_exceptions(aLoadCallback, aManagerWindow);
      }

      if (aView) {
        aManagerWindow.loadView(aView);
      }

      Assert.notEqual(
        aManagerWindow,
        null,
        "Should have an add-ons manager window"
      );
      is(
        aManagerWindow.location.href,
        MANAGER_URI,
        "Should be displaying the correct UI"
      );

      await promiseFocus(aManagerWindow);
      info("window has focus, waiting for manager load");
      await wait_for_manager_load(aManagerWindow);
      info("Manager waiting for view load");
      await wait_for_view_load(aManagerWindow, nullnull, aLongerTimeout);
      resolve(aManagerWindow);
    }

    info("Loading manager window in tab");
    Services.obs.addObserver(function observer(aSubject, aTopic) {
      Services.obs.removeObserver(observer, aTopic);
      if (aSubject.location.href != MANAGER_URI) {
        info("Ignoring load event for " + aSubject.location.href);
        return;
      }
      setup_manager(aSubject);
    }, "EM-loaded");

    aWin.gBrowser.selectedTab = BrowserTestUtils.addTab(aWin.gBrowser);
    aWin.switchToTabHavingURI(MANAGER_URI, true, {
      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    });
  });

  // The promise resolves with the manager window, so it is passed to the callback
  return log_callback(p, aCallback);
}

function close_manager(aManagerWindow, aCallback, aLongerTimeout) {
  let p = new Promise((resolve, reject) => {
    requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2);

    Assert.notEqual(
      aManagerWindow,
      null,
      "Should have an add-ons manager window to close"
    );
    is(
      aManagerWindow.location.href,
      MANAGER_URI,
      "Should be closing window with correct URI"
    );

    aManagerWindow.addEventListener("unload"function listener() {
      try {
        dump("Manager window unload handler\n");
        this.removeEventListener("unload", listener);
        resolve();
      } catch (e) {
        reject(e);
      }
    });
  });

  info("Telling manager window to close");
  aManagerWindow.close();
  info("Manager window close() call returned");

  return log_callback(p, aCallback);
}

function restart_manager(aManagerWindow, aView, aCallback, aLoadCallback) {
  if (!aManagerWindow) {
    return open_manager(aView, aCallback, aLoadCallback);
  }

  return close_manager(aManagerWindow).then(() =>
    open_manager(aView, aCallback, aLoadCallback)
  );
}

function wait_for_window_open(aCallback) {
  let p = new Promise(resolve => {
    Services.wm.addListener({
      onOpenWindow(aXulWin) {
        Services.wm.removeListener(this);

        let domwindow = aXulWin.docShell.domWindow;
        domwindow.addEventListener(
          "load",
          function () {
            executeSoon(function () {
              resolve(domwindow);
            });
          },
          { once: true }
        );
      },

      onCloseWindow() {},
    });
  });

  return log_callback(p, aCallback);
}

function formatDate(aDate) {
  const dtOptions = { year: "numeric", month: "long", day: "numeric" };
  return aDate.toLocaleDateString(undefined, dtOptions);
}

function is_hidden(aElement) {
  var style = aElement.ownerGlobal.getComputedStyle(aElement);
  if (style.display == "none") {
    return true;
  }
  if (style.visibility != "visible") {
    return true;
  }

  // Hiding a parent element will hide all its children
  if (aElement.parentNode != aElement.ownerDocument) {
    return is_hidden(aElement.parentNode);
  }

  return false;
}

function is_element_visible(aElement, aMsg) {
  isnot(aElement, null"Element should not be null, when checking visibility");
  ok(!is_hidden(aElement), aMsg || aElement + " should be visible");
}

function is_element_hidden(aElement, aMsg) {
  isnot(aElement, null"Element should not be null, when checking visibility");
  ok(is_hidden(aElement), aMsg || aElement + " should be hidden");
}

function promiseAddonByID(aId) {
  return AddonManager.getAddonByID(aId);
}

function promiseAddonsByIDs(aIDs) {
  return AddonManager.getAddonsByIDs(aIDs);
}
/**
 * Install an add-on and call a callback when complete.
 *
 * The callback will receive the Addon for the installed add-on.
 */

async function install_addon(path, cb, pathPrefix = TESTROOT) {
  let install = await AddonManager.getInstallForURL(pathPrefix + path);
  let p = new Promise(resolve => {
    install.addListener({
      onInstallEnded: () => resolve(install.addon),
    });

    install.install();
  });

  return log_callback(p, cb);
}

function CategoryUtilities(aManagerWindow) {
  this.window = aManagerWindow;
  this.window.addEventListener("unload", () => (this.window = null), {
    once: true,
  });
}

CategoryUtilities.prototype = {
  window: null,

  get _categoriesBox() {
    return this.window.document.querySelector("categories-box");
  },

  getSelectedViewId() {
    let selectedItem = this._categoriesBox.querySelector("[selected]");
    isnot(selectedItem, null"A category should be selected");
    return selectedItem.getAttribute("viewid");
  },

  get selectedCategory() {
    isnot(
      this.window,
      null,
      "Should not get selected category when manager window is not loaded"
    );
    let viewId = this.getSelectedViewId();
    let view = this.window.gViewController.parseViewId(viewId);
    return view.type == "list" ? view.param : view.type;
  },

  get(categoryType) {
    isnot(
      this.window,
      null,
      "Should not get category when manager window is not loaded"
    );

    let button = this._categoriesBox.querySelector(`[name="${categoryType}"]`);
    if (button) {
      return button;
    }

    ok(false"Should have found a category with type " + categoryType);
    return null;
  },

  isVisible(categoryButton) {
    isnot(
      this.window,
      null,
      "Should not check visible state when manager window is not loaded"
    );

    // There are some tests checking this before the categories have loaded.
    if (!categoryButton) {
      return false;
    }

    if (categoryButton.disabled || categoryButton.hidden) {
      return false;
    }

    return !is_hidden(categoryButton);
  },

  isTypeVisible(categoryType) {
    return this.isVisible(this.get(categoryType));
  },

  open(categoryButton) {
    isnot(
      this.window,
      null,
      "Should not open category when manager window is not loaded"
    );
    ok(
      this.isVisible(categoryButton),
      "Category should be visible if attempting to open it"
    );

    EventUtils.synthesizeMouseAtCenter(categoryButton, {}, this.window);

    // Use wait_for_view_load until all open_manager calls are gone.
    return wait_for_view_load(this.window);
  },

  openType(categoryType) {
    return this.open(this.get(categoryType));
  },
};

// Returns a promise that will resolve when the certificate error override has been added, or reject
// if there is some failure.
function addCertOverride(host) {
  return new Promise((resolve, reject) => {
    let req = new XMLHttpRequest();
    req.open("GET""https://" + host + "/");
    req.onload = reject;
    req.onerror = () => {
      if (req.channel && req.channel.securityInfo) {
        let securityInfo = req.channel.securityInfo;
        if (securityInfo.serverCert) {
          let cos = Cc["@mozilla.org/security/certoverride;1"].getService(
            Ci.nsICertOverrideService
          );
          cos.rememberValidityOverride(
            host,
            -1,
            {},
            securityInfo.serverCert,
            false
          );
          resolve();
          return;
        }
      }
      reject();
    };
    req.send(null);
  });
}

// Returns a promise that will resolve when the necessary certificate overrides have been added.
function addCertOverrides() {
  return Promise.all([
    addCertOverride("nocert.example.com"),
    addCertOverride("self-signed.example.com"),
    addCertOverride("untrusted.example.com"),
    addCertOverride("expired.example.com"),
  ]);
}

/** *** Mock Provider *****/

function MockProvider(
  addonTypes,
  { supportsOperationsRequiringRestart = false } = {}
) {
  this.addons = [];
  this.installs = [];
  this.addonTypes = addonTypes ?? ["extension"];
  // NOTE: operationsRequiringRestart is an historical feature of the
  // XPIProvider, which is not supported anymore, there may still be
  // tests making assumptions about MockProvider behaviors related to
  // it, and so this is a temporary measure to gradually remove the
  // remaining bits of the deprecated feature from the MockProvider
  // test helpers.
  //
  // TODO: (Bug 1921875) Remove operationsRequiringRestart-related
  // behaviors from MockProvider.
  this.supportsOperationsRequiringRestart = supportsOperationsRequiringRestart;

  var self = this;
  registerCleanupFunction(function () {
    if (self.started) {
      self.unregister();
    }
  });

  this.register();
}

MockProvider.prototype = {
  addons: null,
  installs: null,
  addonTypes: null,
  started: null,
  queryDelayPromise: Promise.resolve(),

  blockQueryResponses() {
    this.queryDelayPromise = new Promise(resolve => {
      this._unblockQueries = resolve;
    });
  },

  unblockQueryResponses() {
    if (this._unblockQueries) {
      this._unblockQueries();
      this._unblockQueries = null;
    } else {
      throw new Error("Queries are not blocked");
    }
  },

  /** *** Utility functions *****/

  /**
   * Register this provider with the AddonManager
   */

  register: function MP_register() {
    info("Registering mock add-on provider");
    // addonTypes is supposedly the full set of types supported by the provider.
    // The current list is not complete (there are tests that mock add-on types
    // other than "extension"), but it doesn't affect tests since addonTypes is
    // mainly used to determine whether any of the AddonManager's providers
    // support a type, and XPIProvider already defines the types of interest.
    AddonManagerPrivate.registerProvider(thisthis.addonTypes);
  },

  /**
   * Unregister this provider with the AddonManager
   */

  unregister: function MP_unregister() {
    info("Unregistering mock add-on provider");
    AddonManagerPrivate.unregisterProvider(this);
  },

  /**
   * Adds an add-on to the list of add-ons that this provider exposes to the
   * AddonManager, dispatching appropriate events in the process.
   *
   * @param  aAddon
   *         The add-on to add
   */

  addAddon: function MP_addAddon(aAddon) {
    var oldAddons = this.addons.filter(aOldAddon => aOldAddon.id == aAddon.id);
    var oldAddon = oldAddons.length ? oldAddons[0] : null;

    this.addons = this.addons.filter(aOldAddon => aOldAddon.id != aAddon.id);

    this.addons.push(aAddon);
    aAddon._provider = this;

    if (!this.started) {
      return;
    }

    let requiresRestart = this.supportsOperationsRequiringRestart
      ? (aAddon.operationsRequiringRestart &
          AddonManager.OP_NEEDS_RESTART_INSTALL) !=
        0
      : false;
    AddonManagerPrivate.callInstallListeners(
      "onExternalInstall",
      null,
      aAddon,
      oldAddon,
      requiresRestart
    );
  },

  /**
   * Removes an add-on from the list of add-ons that this provider exposes to
   * the AddonManager, dispatching the onUninstalled event in the process.
   *
   * @param  aAddon
   *         The add-on to add
   */

  removeAddon: function MP_removeAddon(aAddon) {
    var pos = this.addons.indexOf(aAddon);
    if (pos == -1) {
      ok(
        false,
        "Tried to remove an add-on that wasn't registered with the mock provider"
      );
      return;
    }

    this.addons.splice(pos, 1);

    if (!this.started) {
      return;
    }

    AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon);
  },

  /**
   * Adds an add-on install to the list of installs that this provider exposes
   * to the AddonManager, dispatching appropriate events in the process.
   *
   * @param  aInstall
   *         The add-on install to add
   */

  addInstall: function MP_addInstall(aInstall) {
    this.installs.push(aInstall);
    aInstall._provider = this;

    if (!this.started) {
      return;
    }

    aInstall.callListeners("onNewInstall");
  },

  removeInstall: function MP_removeInstall(aInstall) {
    var pos = this.installs.indexOf(aInstall);
    if (pos == -1) {
      ok(
        false,
        "Tried to remove an install that wasn't registered with the mock provider"
      );
      return;
    }

    this.installs.splice(pos, 1);
  },

  /**
   * Creates a set of mock add-on objects and adds them to the list of add-ons
   * managed by this provider.
   *
   * @param  aAddonProperties
   *         An array of objects containing properties describing the add-ons
   * @return Array of the new MockAddons
   */

  createAddons: function MP_createAddons(aAddonProperties) {
    var newAddons = [];
    for (let addonProp of aAddonProperties) {
      if (!this.supportsOperationsRequiringRestart) {
        if (addonProp.operationsRequiringRestart !== undefined) {
          throw new Error(
            `Unexpected operationsRequiringRestart set on MockAddon ${addonProp.id}. MockProvider instance does not support operationsRequiringRestart.`
          );
        }
        addonProp.operationsRequiringRestart = 0;
      }
      let addon = new MockAddon(addonProp.id);
      for (let prop in addonProp) {
        if (prop == "id") {
          continue;
        }
        if (prop == "applyBackgroundUpdates") {
          addon._applyBackgroundUpdates = addonProp[prop];
        } else if (prop == "appDisabled") {
          addon._appDisabled = addonProp[prop];
        } else if (prop == "userDisabled") {
          addon.setUserDisabled(addonProp[prop]);
        } else if (prop == "softDisabled") {
          addon.setSoftDisabled(addonProp[prop]);
        } else {
          addon[prop] = addonProp[prop];
        }
      }
      if (!addon.optionsType && !!addon.optionsURL) {
        addon.optionsType = AddonManager.OPTIONS_TYPE_DIALOG;
      }

      // Make sure the active state matches the passed in properties
      addon.isActive = addon.shouldBeActive;

      this.addAddon(addon);
      newAddons.push(addon);
    }

    return newAddons;
  },

  /**
   * Creates a set of mock add-on install objects and adds them to the list
   * of installs managed by this provider.
   *
   * @param  aInstallProperties
   *         An array of objects containing properties describing the installs
   * @return Array of the new MockInstalls
   */

  createInstalls: function MP_createInstalls(aInstallProperties) {
    var newInstalls = [];
    for (let installProp of aInstallProperties) {
      let install = new MockInstall(
        installProp.name || null,
        installProp.type || null,
        null
      );
      for (let prop in installProp) {
        switch (prop) {
          case "name":
          case "type":
            break;
          case "sourceURI":
            install[prop] = NetUtil.newURI(installProp[prop]);
            break;
          default:
            install[prop] = installProp[prop];
        }
      }
      this.addInstall(install);
      newInstalls.push(install);
    }

    return newInstalls;
  },

  /** *** AddonProvider implementation *****/

  /**
   * Called to initialize the provider.
   */

  startup: function MP_startup() {
    this.started = true;
  },

  /**
   * Called when the provider should shutdown.
   */

  shutdown: function MP_shutdown() {
    this.started = false;
  },

  /**
   * Called to get an Addon with a particular ID.
   *
   * @param  aId
   *         The ID of the add-on to retrieve
   */

  async getAddonByID(aId) {
    await this.queryDelayPromise;

    for (let addon of this.addons) {
      if (addon.id == aId) {
        return addon;
      }
    }

    return null;
  },

  /**
   * Called to get Addons of a particular type.
   *
   * @param  aTypes
   *         An array of types to fetch. Can be null to get all types.
   */

  async getAddonsByTypes(aTypes) {
    await this.queryDelayPromise;

    var addons = this.addons.filter(function (aAddon) {
      if (aTypes && !!aTypes.length && !aTypes.includes(aAddon.type)) {
        return false;
      }
      return true;
    });
    return addons;
  },

  /**
   * Called to get the current AddonInstalls, optionally restricting by type.
   *
   * @param  aTypes
   *         An array of types or null to get all types
   */

  async getInstallsByTypes(aTypes) {
    await this.queryDelayPromise;

    var installs = this.installs.filter(function (aInstall) {
      // Appear to have actually removed cancelled installs from the provider
      if (aInstall.state == AddonManager.STATE_CANCELLED) {
        return false;
      }

      if (aTypes && !!aTypes.length && !aTypes.includes(aInstall.type)) {
        return false;
      }

      return true;
    });
    return installs;
  },

  /**
   * Called when a new add-on has been enabled when only one add-on of that type
   * can be enabled.
   *
   * @param  aId
   *         The ID of the newly enabled add-on
   * @param  aType
   *         The type of the newly enabled add-on
   * @param  aPendingRestart
   *         true if the newly enabled add-on will only become enabled after a
   *         restart
   */

  addonChanged: function MP_addonChanged() {
    // Not implemented
  },

  /**
   * Update the appDisabled property for all add-ons.
   */

  updateAddonAppDisabledStates: function MP_updateAddonAppDisabledStates() {
    // Not needed
  },

  /**
   * Called to get an AddonInstall to download and install an add-on from a URL.
   *
   * @param  {string} aUrl
   *         The URL to be installed
   * @param  {object} aOptions
   *         Options for the install
   */

  getInstallForURL: function MP_getInstallForURL() {
    // Not yet implemented
  },

  /**
   * Called to get an AddonInstall to install an add-on from a local file.
   *
   * @param  aFile
   *         The file to be installed
   */

  getInstallForFile: function MP_getInstallForFile() {
    // Not yet implemented
  },

  /**
   * Called to test whether installing add-ons is enabled.
   *
   * @return true if installing is enabled
   */

  isInstallEnabled: function MP_isInstallEnabled() {
    return false;
  },

  /**
   * Called to test whether this provider supports installing a particular
   * mimetype.
   *
   * @param  aMimetype
   *         The mimetype to check for
   * @return true if the mimetype is supported
   */

  supportsMimetype: function MP_supportsMimetype() {
    return false;
  },

  /**
   * Called to test whether installing add-ons from a URI is allowed.
   *
   * @param  aUri
   *         The URI being installed from
   * @return true if installing is allowed
   */

  isInstallAllowed: function MP_isInstallAllowed() {
    return false;
  },
};

/** *** Mock Addon object for the Mock Provider *****/

function MockAddon(aId, aName, aType, aOperationsRequiringRestart) {
  // Only set required attributes.
  this.id = aId || "";
  this.name = aName || "";
  this.type = aType || "extension";
  this.version = "";
  this.isCompatible = true;
  this.providesUpdatesSecurely = true;
  this.blocklistState = 0;
  this._appDisabled = false;
  this._userDisabled = false;
  this._applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
  this.scope = AddonManager.SCOPE_PROFILE;
  this.isActive = true;
  this.creator = "";
  this.pendingOperations = 0;
  this._permissions =
    AddonManager.PERM_CAN_UNINSTALL |
    AddonManager.PERM_CAN_ENABLE |
    AddonManager.PERM_CAN_DISABLE |
    AddonManager.PERM_CAN_UPGRADE |
    AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS;
  this.operationsRequiringRestart =
    aOperationsRequiringRestart != undefined
      ? aOperationsRequiringRestart
      : AddonManager.OP_NEEDS_RESTART_INSTALL |
        AddonManager.OP_NEEDS_RESTART_UNINSTALL |
        AddonManager.OP_NEEDS_RESTART_ENABLE |
        AddonManager.OP_NEEDS_RESTART_DISABLE;
}

MockAddon.prototype = {
  get isCorrectlySigned() {
    if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED) {
      return true;
    }
    return this.signedState > AddonManager.SIGNEDSTATE_MISSING;
  },

  get shouldBeActive() {
    return (
      !this.appDisabled &&
      !this._userDisabled &&
      !this._softDisabled &&
      !(this.pendingOperations & AddonManager.PENDING_UNINSTALL)
    );
  },

  get appDisabled() {
    return this._appDisabled;
  },

  set appDisabled(val) {
    if (val == this._appDisabled) {
      return;
    }

    AddonManagerPrivate.callAddonListeners("onPropertyChanged"this, [
      "appDisabled",
    ]);

    var currentActive = this.shouldBeActive;
    this._appDisabled = val;
    var newActive = this.shouldBeActive;
    this._updateActiveState(currentActive, newActive);
  },

  get userDisabled() {
    // NOTE: the logic here should reseamble the logic
    // from the AddonInstall getter with the same name.
    return this._softDisabled || this._userDisabled;
  },

  set userDisabled(val) {
    throw new Error("No. Bad.");
  },

  get softDisabled() {
    return this._softDisabled;
  },

  set softDisabled(val) {
    throw new Error("No. Bad.");
  },

  setUserDisabled(val) {
    // NOTE: the logic here should reseamble the logic
    // from the AddonInstall method with the same name.
    if (val == (this._userDisabled || this._softDisabled)) {
      return;
    }

    var currentActive = this.shouldBeActive;
    this._userDisabled = val;
    var newActive = this.shouldBeActive;
    this._updateActiveState(currentActive, newActive);
  },

  setSoftDisabled(val) {
    // NOTE: the logic here should reseamble the logic
    // from the AddonInstall method with the same name.
    if (val == this._softDisabled) {
      return;
    }

    var currentActive = this.shouldBeActive;
    if (!this._userDisabled) {
      this._softDisabled = val;
    }
    var newActive = this.shouldBeActive;
    this._updateActiveState(currentActive, newActive);
  },

  async enable() {
    await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));

    this.setUserDisabled(false);
    this.setSoftDisabled(false);
  },
  async disable() {
    await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));

    this.setUserDisabled(true);
  },

  get permissions() {
    let permissions = this._permissions;
    if (this.appDisabled) {
      permissions &= ~AddonManager.PERM_CAN_ENABLE;
      permissions &= ~AddonManager.PERM_CAN_DISABLE;
    }
    return permissions;
  },

  set permissions(val) {
    this._permissions = val;
  },

  get applyBackgroundUpdates() {
    return this._applyBackgroundUpdates;
  },

  set applyBackgroundUpdates(val) {
    if (
      val != AddonManager.AUTOUPDATE_DEFAULT &&
      val != AddonManager.AUTOUPDATE_DISABLE &&
      val != AddonManager.AUTOUPDATE_ENABLE
    ) {
      ok(false"addon.applyBackgroundUpdates set to an invalid value: " + val);
    }
    this._applyBackgroundUpdates = val;
    AddonManagerPrivate.callAddonListeners("onPropertyChanged"this, [
      "applyBackgroundUpdates",
    ]);
  },

  isCompatibleWith() {
    return true;
  },

  findUpdates() {
    // Tests can implement this if they need to
  },

  async getBlocklistURL() {
    return this.blocklistURL;
  },

  uninstall(aAlwaysAllowUndo = false) {
    if (
      this.operationsRequiringRestart &
        AddonManager.OP_NEED_RESTART_UNINSTALL &&
      this.pendingOperations & AddonManager.PENDING_UNINSTALL
    ) {
      throw Components.Exception("Add-on is already pending uninstall");
    }

    var needsRestart =
      aAlwaysAllowUndo ||
      !!(
        this.operationsRequiringRestart &
        AddonManager.OP_NEEDS_RESTART_UNINSTALL
      );
    this.pendingOperations |= AddonManager.PENDING_UNINSTALL;
    AddonManagerPrivate.callAddonListeners(
      "onUninstalling",
      this,
      needsRestart
    );
    if (!needsRestart) {
      this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
      this._provider.removeAddon(this);
    } else if (
      !(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE)
    ) {
      this.isActive = false;
    }
  },

  cancelUninstall() {
    if (!(this.pendingOperations & AddonManager.PENDING_UNINSTALL)) {
      throw Components.Exception("Add-on is not pending uninstall");
    }

    this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
    this.isActive = this.shouldBeActive;
    AddonManagerPrivate.callAddonListeners("onOperationCancelled"this);
  },

  markAsSeen() {
    this.seen = true;
  },

  updateBlocklistState() {
    // NOTE: this is currently a no-op meant to just prevent MockProvider
    // addons to trigger an unexpected "addon.updateBlockistState is not a function"
    // error in tests covering the blocklist (while there are also MockProvider
    // installed addons).
  },

  _updateActiveState(currentActive, newActive) {
    if (currentActive == newActive) {
      return;
    }

    if (newActive == this.isActive) {
      this.pendingOperations -= newActive
        ? AddonManager.PENDING_DISABLE
        : AddonManager.PENDING_ENABLE;
      AddonManagerPrivate.callAddonListeners("onOperationCancelled"this);
    } else if (newActive) {
      let needsRestart = !!(
        this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_ENABLE
      );
      this.pendingOperations |= AddonManager.PENDING_ENABLE;
      AddonManagerPrivate.callAddonListeners("onEnabling"this, needsRestart);
      if (!needsRestart) {
        this.isActive = newActive;
        this.pendingOperations -= AddonManager.PENDING_ENABLE;
        AddonManagerPrivate.callAddonListeners("onEnabled"this);
      }
    } else {
      let needsRestart = !!(
        this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE
      );
      this.pendingOperations |= AddonManager.PENDING_DISABLE;
      AddonManagerPrivate.callAddonListeners("onDisabling"this, needsRestart);
      if (!needsRestart) {
        this.isActive = newActive;
        this.pendingOperations -= AddonManager.PENDING_DISABLE;
        AddonManagerPrivate.callAddonListeners("onDisabled"this);
      }
    }
  },
};

/** *** Mock AddonInstall object for the Mock Provider *****/

function MockInstall(aName, aType, aAddonToInstall) {
  this.name = aName || "";
  // Don't expose type until download completed
  this._type = aType || "extension";
  this.type = null;
  this.version = "1.0";
  this.iconURL = "";
  this.infoURL = "";
  this.state = AddonManager.STATE_AVAILABLE;
  this.error = 0;
  this.sourceURI = null;
  this.file = null;
  this.progress = 0;
  this.maxProgress = -1;
  this.certificate = null;
  this.certName = "";
  this.existingAddon = null;
  this.addon = null;
  this._addonToInstall = aAddonToInstall;
  this.listeners = [];

  // Another type of install listener for tests that want to check the results
  // of code run from standard install listeners
  this.testListeners = [];
}

MockInstall.prototype = {
  install() {
    switch (this.state) {
      case AddonManager.STATE_AVAILABLE:
        this.state = AddonManager.STATE_DOWNLOADING;
        if (!this.callListeners("onDownloadStarted")) {
          this.state = AddonManager.STATE_CANCELLED;
          this.callListeners("onDownloadCancelled");
          return;
        }

        this.type = this._type;

        // Adding addon to MockProvider to be implemented when needed
        if (this._addonToInstall) {
          this.addon = this._addonToInstall;
        } else {
          this.addon = new MockAddon(""this.name, this.type);
          this.addon.version = this.version;
          this.addon.pendingOperations = AddonManager.PENDING_INSTALL;
        }
        this.addon.install = this;
        if (this.existingAddon) {
          if (!this.addon.id) {
            this.addon.id = this.existingAddon.id;
          }
          this.existingAddon.pendingUpgrade = this.addon;
          this.existingAddon.pendingOperations |= AddonManager.PENDING_UPGRADE;
        }

        this.state = AddonManager.STATE_DOWNLOADED;
        this.callListeners("onDownloadEnded");
      // fall through
      case AddonManager.STATE_DOWNLOADED: {
        this.state = AddonManager.STATE_INSTALLING;
        if (!this.callListeners("onInstallStarted")) {
          this.state = AddonManager.STATE_CANCELLED;
          this.callListeners("onInstallCancelled");
          return;
        }

        let needsRestart = this._provider?.supportsOperationsRequiringRestart
          ? this.operationsRequiringRestart &
            AddonManager.OP_NEEDS_RESTART_INSTALL
          : false;
        AddonManagerPrivate.callAddonListeners(
          "onInstalling",
          this.addon,
          needsRestart
        );
        if (!needsRestart) {
          AddonManagerPrivate.callAddonListeners("onInstalled"this.addon);
        }

        this.state = AddonManager.STATE_INSTALLED;
        this.callListeners("onInstallEnded");
        break;
      }
      case AddonManager.STATE_DOWNLOADING:
      case AddonManager.STATE_CHECKING_UPDATE:
      case AddonManager.STATE_INSTALLING:
        // Installation is already running
        return;
      default:
        ok(false"Cannot start installing when state = " + this.state);
    }
  },

  cancel() {
    switch (this.state) {
      case AddonManager.STATE_AVAILABLE:
        this.state = AddonManager.STATE_CANCELLED;
        break;
      case AddonManager.STATE_INSTALLED:
        this.state = AddonManager.STATE_CANCELLED;
        this._provider.removeInstall(this);
        this.callListeners("onInstallCancelled");
        break;
      default:
        // Handling cancelling when downloading to be implemented when needed
        ok(false"Cannot cancel when state = " + this.state);
    }
  },

  addListener(aListener) {
    if (!this.listeners.some(i => i == aListener)) {
      this.listeners.push(aListener);
    }
  },

  removeListener(aListener) {
    this.listeners = this.listeners.filter(i => i != aListener);
  },

  addTestListener(aListener) {
    if (!this.testListeners.some(i => i == aListener)) {
      this.testListeners.push(aListener);
    }
  },

  removeTestListener(aListener) {
    this.testListeners = this.testListeners.filter(i => i != aListener);
  },

  callListeners(aMethod) {
    var result = AddonManagerPrivate.callInstallListeners(
      aMethod,
      this.listeners,
      this,
      this.addon
    );

    // Call test listeners after standard listeners to remove race condition
    // between standard and test listeners
    for (let listener of this.testListeners) {
      try {
        if (aMethod in listener) {
          if (listener[aMethod](thisthis.addon) === false) {
            result = false;
          }
        }
      } catch (e) {
        ok(false"Test listener threw exception: " + e);
      }
    }

    return result;
  },
};

function waitForCondition(condition, nextTest, errorMsg) {
  let tries = 0;
  let interval = setInterval(function () {
    if (tries >= 30) {
      ok(false, errorMsg);
      moveOn();
    }
    var conditionPassed;
    try {
      conditionPassed = condition();
    } catch (e) {
      ok(false, e + "\n" + e.stack);
      conditionPassed = false;
    }
    if (conditionPassed) {
      moveOn();
    }
    tries++;
  }, 100);
  let moveOn = function () {
    clearInterval(interval);
    nextTest();
  };
}

// Wait for and then acknowledge (by pressing the primary button) the
// given notification.
function promiseNotification(id = "addon-webext-permissions") {
  return new Promise(resolve => {
    function popupshown() {
      let notification = PopupNotifications.getNotification(id);
      if (notification) {
        PopupNotifications.panel.removeEventListener("popupshown", popupshown);
        PopupNotifications.panel.firstElementChild.button.click();
        resolve();
      }
    }
    PopupNotifications.panel.addEventListener("popupshown", popupshown);
  });
}

/**
 * Wait for the given PopupNotification to display
 *
 * @param {string} name
 *        The name of the notification to wait for.
 *
 * @returns {Promise}
 *          Resolves with the notification window.
 */

function promisePopupNotificationShown(name = "addon-webext-permissions") {
  return new Promise(resolve => {
    function popupshown() {
      let notification = PopupNotifications.getNotification(name);
      if (!notification) {
        return;
      }

      ok(notification, `${name} notification shown`);
      ok(PopupNotifications.isPanelOpen, "notification panel open");

      PopupNotifications.panel.removeEventListener("popupshown", popupshown);
      resolve(PopupNotifications.panel.firstChild);
    }
    PopupNotifications.panel.addEventListener("popupshown", popupshown);
  });
}

function waitAppMenuNotificationShown(
  id,
  addonId,
  accept = false,
  win = window
) {
  const { AppMenuNotifications } = ChromeUtils.importESModule(
    "resource://gre/modules/AppMenuNotifications.sys.mjs"
  );
  return new Promise(resolve => {
    let { document, PanelUI } = win;

    async function popupshown() {
      let notification = AppMenuNotifications.activeNotification;
      if (!notification) {
        return;
      }

      is(notification.id, id, `${id} notification shown`);
      ok(PanelUI.isNotificationPanelOpen, "notification panel open");

      PanelUI.notificationPanel.removeEventListener("popupshown", popupshown);

      if (id == "addon-installed" && addonId) {
        let addon = await AddonManager.getAddonByID(addonId);
        if (!addon) {
          ok(false, `Addon with id "${addonId}" not found`);
        }

        let checkbox = document.getElementById("addon-incognito-checkbox");
        if (ExtensionsUI.POSTINSTALL_PRIVATEBROWSING_CHECKBOX) {
          let hidden = !(
            addon.permissions &
            AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
          );
          is(checkbox.hidden, hidden, "checkbox visibility is correct");
        } else {
          is(
            checkbox.hidden,
            true,
            "incognito checkbox expected to be hidden in the post install dialog"
          );
        }
      }

      let popupnotificationID = PanelUI._getPopupId(notification);
      let popupnotification = document.getElementById(popupnotificationID);

      if (accept) {
        popupnotification.button.click();
      }

      resolve(popupnotification);
    }
    // If it's already open just run the test.
    let notification = AppMenuNotifications.activeNotification;
    if (notification && PanelUI.isNotificationPanelOpen) {
      popupshown();
      return;
    }
    PanelUI.notificationPanel.addEventListener("popupshown", popupshown);
  });
}

function acceptAppMenuNotificationWhenShown(id, addonId) {
  return waitAppMenuNotificationShown(id, addonId, true);
}

/* HTML view helpers */
async function loadInitialView(type, opts) {
  if (type) {
    // Force the first page load to be the view we want.
    let viewId;
    if (type.startsWith("addons://")) {
      viewId = type;
    } else {
      viewId =
        type == "discover" ? "addons://discover/" : `addons://list/${type}`;
    }
    Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, viewId);
  }

  let loadCallback;
  let loadCallbackDone = Promise.resolve();

  if (opts && opts.loadCallback) {
    loadCallback = win => {
      loadCallbackDone = (async () => {
        // Wait for the test code to finish running before proceeding.
        await opts.loadCallback(win);
      })();
    };
  }

  let win = await open_manager(nullnull, loadCallback);
  if (!opts || !opts.withAnimations) {
    win.document.body.setAttribute("skip-animations""");
  }

  // Let any load callback code to run before the rest of the test continues.
  await loadCallbackDone;

  return win;
}

function getSection(doc, className) {
  return doc.querySelector(`section.${className}`);
}

function waitForViewLoad(win) {
  return wait_for_view_load(win, undefined, true);
}

function closeView(win) {
  return close_manager(win);
}

function switchView(win, type) {
  return new CategoryUtilities(win).openType(type);
}

function isCategoryVisible(win, type) {
  return new CategoryUtilities(win).isTypeVisible(type);
}

function mockPromptService() {
  let { prompt } = Services;
  let promptService = {
    // The prompt returns 1 for cancelled and 0 for accepted.
    _response: 1,
    QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
    confirmEx: () => promptService._response,
  };
  Services.prompt = promptService;
  registerCleanupFunction(() => {
    Services.prompt = prompt;
  });
  return promptService;
}

function assertHasPendingUninstalls(addonList, expectedPendingUninstallsCount) {
  const pendingUninstalls = addonList.querySelector(
    "message-bar-stack.pending-uninstall"
  );
  ok(pendingUninstalls, "Got a pending-uninstall message-bar-stack");
  is(
    pendingUninstalls.childElementCount,
    expectedPendingUninstallsCount,
    "Got a message bar in the pending-uninstall message-bar-stack"
  );
}

function assertHasPendingUninstallAddon(addonList, addon) {
  const pendingUninstalls = addonList.querySelector(
    "message-bar-stack.pending-uninstall"
  );
  const addonPendingUninstall = addonList.getPendingUninstallBar(addon);
  ok(
    addonPendingUninstall,
    "Got expected message-bar for the pending uninstall test extension"
  );
  is(
    addonPendingUninstall.parentNode,
    pendingUninstalls,
    "pending uninstall bar should be part of the message-bar-stack"
  );
  is(
    addonPendingUninstall.getAttribute("addon-id"),
    addon.id,
    "Got expected addon-id attribute on the pending uninstall message-bar"
  );
}

async function testUndoPendingUninstall(addonList, addon) {
  const addonPendingUninstall = addonList.getPendingUninstallBar(addon);
  const undoButton = addonPendingUninstall.querySelector("button[action=undo]");
  ok(undoButton, "Got undo action button in the pending uninstall message-bar");

  info(
    "Clicking the pending uninstall undo button and wait for addon card rendered"
  );
  const updated = BrowserTestUtils.waitForEvent(addonList, "add");
  undoButton.click();
  await updated;

  ok(
    addon && !(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
    "The addon pending uninstall cancelled"
  );
}

function loadTestSubscript(filePath) {
  Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
}

function cleanupPendingNotifications() {
  const { ExtensionsUI } = ChromeUtils.importESModule(
    "resource:///modules/ExtensionsUI.sys.mjs"
  );
  info("Cleanup any pending notification before exiting the test");
  const keys = ChromeUtils.nondeterministicGetWeakSetKeys(
    ExtensionsUI.pendingNotifications
  );
  if (keys) {
    keys.forEach(key => ExtensionsUI.pendingNotifications.delete(key));
  }
}

function promisePermissionPrompt(addonId) {
  return BrowserUtils.promiseObserved(
    "webextension-permission-prompt",
    subject => {
      const { info } = subject.wrappedJSObject || {};
      return !addonId || (info.addon && info.addon.id === addonId);
    }
  ).then(({ subject }) => {
    return subject.wrappedJSObject.info;
  });
}

async function handlePermissionPrompt({
  addonId,
  reject = false,
  assertIcon = true,
} = {}) {
  const info = await promisePermissionPrompt(addonId);
  // Assert that info.addon and info.icon are defined as expected.
  is(
    info.addon && info.addon.id,
    addonId,
    "Got the AddonWrapper in the permission prompt info"
  );

  if (assertIcon) {
    Assert.notEqual(
      info.icon,
      null,
      "Got an addon icon in the permission prompt info"
    );
  }

  if (reject) {
    info.reject();
  } else {
    info.resolve();
  }
}

async function switchToDetailView({ id, win }) {
  let card = getAddonCard(win, id);
  ok(card, `Addon card found for ${id}`);
  ok(!card.querySelector("addon-details"), "The card doesn't have details");
  let loaded = waitForViewLoad(win);
  EventUtils.synthesizeMouseAtCenter(
    card.querySelector(".addon-name-link"),
    { clickCount: 1 },
    win
  );
  await loaded;
  card = getAddonCard(win, id);
  ok(card.querySelector("addon-details"), "The card does have details");
  return card;
}

Messung V0.5
C=94 H=96 G=94

¤ Dauer der Verarbeitung: 0.24 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.