Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  browser_midi_permission_gated.js   Sprache: JAVA

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


const EXAMPLE_COM_URL =
  "https://example.com/document-builder.sjs?html=

Test midi permission with synthetic site permission addon

";

const PAGE_WITH_IFRAMES_URL = `https://example.org/document-builder.sjs?html=
  <h1>Test midi permission with synthetic site permission addon in iframes</h1>
  <iframe id=sameOrigin src="${encodeURIComponent(
    'https://example.org/document-builder.sjs?html=SameOrigin"'
  )}">
  <iframe id=crossOrigin  src="${encodeURIComponent(
    'https://example.net/document-builder.sjs?html=CrossOrigin"'
  )}">`;

const l10n = new Localization(
  [
    "browser/addonNotifications.ftl",
    "toolkit/global/extensions.ftl",
    "toolkit/global/extensionPermissions.ftl",
    "branding/brand.ftl",
  ],
  true
);

const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);
ChromeUtils.defineESModuleGetters(this, {
  AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
});

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["midi.prompt.testing"false]],
  });

  AddonTestUtils.initMochitest(this);
  AddonTestUtils.hookAMTelemetryEvents();

  // Once the addon is installed, a dialog is displayed as a confirmation.
  // This could interfere with tests running after this one, so we set up a listener
  // that will always accept post install dialogs so we don't have  to deal with them in
  // the test.
  alwaysAcceptAddonPostInstallDialogs();

  registerCleanupFunction(async () => {
    // Remove the permission.
    await SpecialPowers.removePermission("midi-sysex", {
      url: EXAMPLE_COM_URL,
    });
    await SpecialPowers.removePermission("midi-sysex", {
      url: PAGE_WITH_IFRAMES_URL,
    });
    await SpecialPowers.removePermission("midi", {
      url: EXAMPLE_COM_URL,
    });
    await SpecialPowers.removePermission("midi", {
      url: PAGE_WITH_IFRAMES_URL,
    });
    await SpecialPowers.removePermission("install", {
      url: EXAMPLE_COM_URL,
    });

    while (gBrowser.tabs.length > 1) {
      BrowserTestUtils.removeTab(gBrowser.selectedTab);
    }
  });
});

add_task(async function testRequestMIDIAccess() {
  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, EXAMPLE_COM_URL);
  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
  const testPageHost = gBrowser.selectedTab.linkedBrowser.documentURI.host;
  Services.fog.testResetFOG();

  info("Check that midi-sysex isn't set");
  ok(
    await SpecialPowers.testPermission(
      "midi-sysex",
      SpecialPowers.Services.perms.UNKNOWN_ACTION,
      { url: EXAMPLE_COM_URL }
    ),
    "midi-sysex value should have UNKNOWN permission"
  );

  info("Request midi-sysex access");
  let onAddonInstallBlockedNotification = waitForNotification(
    "addon-install-blocked"
  );
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
      sysex: true,
    });
  });

  info("Deny site permission addon install in first popup");
  let addonInstallPanel = await onAddonInstallBlockedNotification;
  const [installPopupHeader, installPopupMessage] =
    addonInstallPanel.querySelectorAll(
      "description.popup-notification-description"
    );
  is(
    installPopupHeader.textContent,
    l10n.formatValueSync("site-permission-install-first-prompt-midi-header"),
    "First popup has expected header text"
  );
  is(
    installPopupMessage.textContent,
    l10n.formatValueSync("site-permission-install-first-prompt-midi-message"),
    "First popup has expected message"
  );

  let notification = addonInstallPanel.childNodes[0];
  // secondaryButton is the "Don't allow" button
  notification.secondaryButton.click();

  let rejectionMessage = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      let errorMessage;
      try {
        await content.midiAccessRequestPromise;
      } catch (e) {
        errorMessage = `${e.name}: ${e.message}`;
      }

      delete content.midiAccessRequestPromise;
      return errorMessage;
    }
  );
  is(
    rejectionMessage,
    "SecurityError: WebMIDI requires a site permission add-on to activate"
  );

  assertSitePermissionInstallTelemetryEvents(["site_warning""cancelled"]);

  info("Deny site permission addon install in second popup");
  onAddonInstallBlockedNotification = waitForNotification(
    "addon-install-blocked"
  );
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
      sysex: true,
    });
  });
  addonInstallPanel = await onAddonInstallBlockedNotification;
  notification = addonInstallPanel.childNodes[0];
  let dialogPromise = waitForInstallDialog();
  notification.button.click();
  let installDialog = await dialogPromise;
  is(
    installDialog.querySelector(".popup-notification-description").textContent,
    l10n.formatValueSync(
      "webext-site-perms-header-with-gated-perms-midi-sysex",
      { hostname: testPageHost }
    ),
    "Install dialog has expected header text"
  );
  is(
    installDialog.querySelector("popupnotificationcontent description")
      .textContent,
    l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"),
    "Install dialog has expected description"
  );

  // secondaryButton is the "Cancel" button
  installDialog.secondaryButton.click();

  rejectionMessage = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      let errorMessage;
      try {
        await content.midiAccessRequestPromise;
      } catch (e) {
        errorMessage = `${e.name}: ${e.message}`;
      }

      delete content.midiAccessRequestPromise;
      return errorMessage;
    }
  );
  is(
    rejectionMessage,
    "SecurityError: WebMIDI requires a site permission add-on to activate"
  );

  assertSitePermissionInstallTelemetryEvents([
    "site_warning",
    "permissions_prompt",
    "cancelled",
  ]);

  info("Request midi-sysex access again");
  onAddonInstallBlockedNotification = waitForNotification(
    "addon-install-blocked"
  );
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
      sysex: true,
    });
  });

  info("Accept site permission addon install");
  addonInstallPanel = await onAddonInstallBlockedNotification;
  notification = addonInstallPanel.childNodes[0];
  dialogPromise = waitForInstallDialog();
  notification.button.click();
  installDialog = await dialogPromise;
  installDialog.button.click();

  info("Wait for the midi-sysex access request promise to resolve");
  let accessGranted = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      try {
        await content.midiAccessRequestPromise;
        return true;
      } catch (e) {}

      delete content.midiAccessRequestPromise;
      return false;
    }
  );
  ok(accessGranted, "requestMIDIAccess resolved");

  info("Check that midi-sysex is now set");
  ok(
    await SpecialPowers.testPermission(
      "midi-sysex",
      SpecialPowers.Services.perms.ALLOW_ACTION,
      { url: EXAMPLE_COM_URL }
    ),
    "midi-sysex value should have ALLOW permission"
  );
  ok(
    await SpecialPowers.testPermission(
      "midi",
      SpecialPowers.Services.perms.UNKNOWN_ACTION,
      { url: EXAMPLE_COM_URL }
    ),
    "but midi should have UNKNOWN permission"
  );

  info("Check that we don't prompt user again once they installed the addon");
  const accessPromiseState = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      return content.navigator
        .requestMIDIAccess({ sysex: true })
        .then(() => "resolved");
    }
  );
  is(
    accessPromiseState,
    "resolved",
    "requestMIDIAccess resolved without user prompt"
  );

  assertSitePermissionInstallTelemetryEvents([
    "site_warning",
    "permissions_prompt",
    "completed",
  ]);

  info("Request midi access without sysex");
  onAddonInstallBlockedNotification = waitForNotification(
    "addon-install-blocked"
  );
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    content.midiNoSysexAccessRequestPromise =
      content.navigator.requestMIDIAccess();
  });

  info("Accept site permission addon install");
  addonInstallPanel = await onAddonInstallBlockedNotification;
  notification = addonInstallPanel.childNodes[0];

  is(
    notification
      .querySelector("#addon-install-blocked-info")
      .getAttribute("href"),
    Services.urlFormatter.formatURLPref("app.support.baseURL") +
      "site-permission-addons",
    "Got the expected SUMO page as a learn more link in the addon-install-blocked panel"
  );

  dialogPromise = waitForInstallDialog();
  notification.button.click();
  installDialog = await dialogPromise;

  is(
    installDialog.querySelector(".popup-notification-description").textContent,
    l10n.formatValueSync("webext-site-perms-header-with-gated-perms-midi", {
      hostname: testPageHost,
    }),
    "Install dialog has expected header text"
  );
  is(
    installDialog.querySelector("popupnotificationcontent description")
      .textContent,
    l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"),
    "Install dialog has expected description"
  );

  installDialog.button.click();

  info("Wait for the midi access request promise to resolve");
  accessGranted = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      try {
        await content.midiNoSysexAccessRequestPromise;
        return true;
      } catch (e) {}

      delete content.midiNoSysexAccessRequestPromise;
      return false;
    }
  );
  ok(accessGranted, "requestMIDIAccess resolved");

  info("Check that both midi-sysex and midi are now set");
  ok(
    await SpecialPowers.testPermission(
      "midi-sysex",
      SpecialPowers.Services.perms.ALLOW_ACTION,
      { url: EXAMPLE_COM_URL }
    ),
    "midi-sysex value should have ALLOW permission"
  );
  ok(
    await SpecialPowers.testPermission(
      "midi",
      SpecialPowers.Services.perms.ALLOW_ACTION,
      { url: EXAMPLE_COM_URL }
    ),
    "and midi value should also have ALLOW permission"
  );

  assertSitePermissionInstallTelemetryEvents([
    "site_warning",
    "permissions_prompt",
    "completed",
  ]);

  info("Check that we don't prompt user again when they perm denied");
  // remove permission to have a clean state
  await SpecialPowers.removePermission("midi-sysex", {
    url: EXAMPLE_COM_URL,
  });

  onAddonInstallBlockedNotification = waitForNotification(
    "addon-install-blocked"
  );
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
      sysex: true,
    });
  });

  info("Perm-deny site permission addon install");
  addonInstallPanel = await onAddonInstallBlockedNotification;
  // Click the "Report Suspicious Site" menuitem, which has the same effect as
  // "Never Allow" and also submits a telemetry event (which we check below).
  notification.menupopup.querySelectorAll("menuitem")[1].click();

  rejectionMessage = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      let errorMessage;
      try {
        await content.midiAccessRequestPromise;
      } catch (e) {
        errorMessage = e.name;
      }

      delete content.midiAccessRequestPromise;
      return errorMessage;
    }
  );
  is(rejectionMessage, "SecurityError""requestMIDIAccess was rejected");

  info("Request midi-sysex access again");
  let denyIntervalStart = performance.now();
  rejectionMessage = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      let errorMessage;
      try {
        await content.navigator.requestMIDIAccess({
          sysex: true,
        });
      } catch (e) {
        errorMessage = e.name;
      }
      return errorMessage;
    }
  );
  is(
    rejectionMessage,
    "SecurityError",
    "requestMIDIAccess was rejected without user prompt"
  );
  let denyIntervalElapsed = performance.now() - denyIntervalStart;
  Assert.greaterOrEqual(
    denyIntervalElapsed,
    3000,
    `Rejection should be delayed by a randomized interval no less than 3 seconds (got ${
      denyIntervalElapsed / 1000
    } seconds)`
  );

  Assert.deepEqual(
    [{ suspicious_site: "example.com" }],
    AddonTestUtils.getAMGleanEvents("reportSuspiciousSite"),
    "Expected Glean event recorded."
  );

  assertSitePermissionInstallTelemetryEvents(["site_warning""cancelled"]);
});

add_task(async function testIframeRequestMIDIAccess() {
  gBrowser.selectedTab = BrowserTestUtils.addTab(
    gBrowser,
    PAGE_WITH_IFRAMES_URL
  );
  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);

  info("Check that midi-sysex isn't set");
  ok(
    await SpecialPowers.testPermission(
      "midi-sysex",
      SpecialPowers.Services.perms.UNKNOWN_ACTION,
      { url: PAGE_WITH_IFRAMES_URL }
    ),
    "midi-sysex value should have UNKNOWN permission"
  );

  info("Request midi-sysex access from the same-origin iframe");
  const sameOriginIframeBrowsingContext = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      return content.document.getElementById("sameOrigin").browsingContext;
    }
  );

  let onAddonInstallBlockedNotification = waitForNotification(
    "addon-install-blocked"
  );
  await SpecialPowers.spawn(sameOriginIframeBrowsingContext, [], () => {
    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
      sysex: true,
    });
  });

  info("Accept site permission addon install");
  const addonInstallPanel = await onAddonInstallBlockedNotification;
  const notification = addonInstallPanel.childNodes[0];
  const dialogPromise = waitForInstallDialog();
  notification.button.click();
  let installDialog = await dialogPromise;
  installDialog.button.click();

  info("Wait for the midi-sysex access request promise to resolve");
  const accessGranted = await SpecialPowers.spawn(
    sameOriginIframeBrowsingContext,
    [],
    async () => {
      try {
        await content.midiAccessRequestPromise;
        return true;
      } catch (e) {}

      delete content.midiAccessRequestPromise;
      return false;
    }
  );
  ok(accessGranted, "requestMIDIAccess resolved");

  info("Check that midi-sysex is now set");
  ok(
    await SpecialPowers.testPermission(
      "midi-sysex",
      SpecialPowers.Services.perms.ALLOW_ACTION,
      { url: PAGE_WITH_IFRAMES_URL }
    ),
    "midi-sysex value should have ALLOW permission"
  );

  info(
    "Check that we don't prompt user again once they installed the addon from the same-origin iframe"
  );
  const accessPromiseState = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      return content.navigator
        .requestMIDIAccess({ sysex: true })
        .then(() => "resolved");
    }
  );
  is(
    accessPromiseState,
    "resolved",
    "requestMIDIAccess resolved without user prompt"
  );

  assertSitePermissionInstallTelemetryEvents([
    "site_warning",
    "permissions_prompt",
    "completed",
  ]);

  info("Check that request is rejected when done from a cross-origin iframe");
  const crossOriginIframeBrowsingContext = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      return content.document.getElementById("crossOrigin").browsingContext;
    }
  );

  const onConsoleErrorMessage = new Promise(resolve => {
    const errorListener = {
      observe(error) {
        if (error.message.includes("WebMIDI access request was denied")) {
          resolve(error);
          Services.console.unregisterListener(errorListener);
        }
      },
    };
    Services.console.registerListener(errorListener);
  });

  const rejectionMessage = await SpecialPowers.spawn(
    crossOriginIframeBrowsingContext,
    [],
    async () => {
      let errorName;
      try {
        await content.navigator.requestMIDIAccess({
          sysex: true,
        });
      } catch (e) {
        errorName = e.name;
      }
      return errorName;
    }
  );

  is(
    rejectionMessage,
    "SecurityError",
    "requestMIDIAccess from the remote iframe was rejected"
  );

  const consoleErrorMessage = await onConsoleErrorMessage;
  ok(
    consoleErrorMessage.message.includes(
      `WebMIDI access request was denied: ❝SitePermsAddons can't be installed from cross origin subframes❞`,
      "an error message is sent to the console"
    )
  );
  assertSitePermissionInstallTelemetryEvents([]);
});

add_task(async function testRequestMIDIAccessLocalhost() {
  const httpServer = new HttpServer();
  httpServer.start(-1);
  httpServer.registerPathHandler(`/test`, function (request, response) {
    response.setStatusLine(request.httpVersion, 200, "OK");
    response.write(`
      <!DOCTYPE html>
      <meta charset=utf8>
      <h1>Test requestMIDIAccess on lcoalhost</h1>`);
  });
  const localHostTestUrl = `http://localhost:${httpServer.identity.primaryPort}/test`;

  registerCleanupFunction(async function cleanup() {
    await new Promise(resolve => httpServer.stop(resolve));
  });

  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, localHostTestUrl);
  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);

  info("Check that midi-sysex isn't set");
  ok(
    await SpecialPowers.testPermission(
      "midi-sysex",
      SpecialPowers.Services.perms.UNKNOWN_ACTION,
      { url: localHostTestUrl }
    ),
    "midi-sysex value should have UNKNOWN permission"
  );

  info(
    "Request midi-sysex access should not prompt for addon install on locahost, but for permission"
  );
  let popupShown = BrowserTestUtils.waitForEvent(
    PopupNotifications.panel,
    "popupshown"
  );
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
      sysex: true,
    });
  });
  await popupShown;
  is(
    PopupNotifications.panel.querySelector("popupnotification").id,
    "midi-notification",
    "midi notification was displayed"
  );

  info("Accept permission");
  PopupNotifications.panel
    .querySelector(".popup-notification-primary-button")
    .click();

  info("Wait for the midi-sysex access request promise to resolve");
  let accessGranted = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      try {
        await content.midiAccessRequestPromise;
        return true;
      } catch (e) {}

      delete content.midiAccessRequestPromise;
      return false;
    }
  );
  ok(accessGranted, "requestMIDIAccess resolved");

  // We're remembering permission grants temporarily on the tab since Bug 1754005.
  info(
    "Check that a new request is automatically granted because we granted before in the same tab."
  );

  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
    content.navigator.requestMIDIAccess({ sysex: true });
  });

  accessGranted = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      try {
        await content.midiAccessRequestPromise;
        return true;
      } catch (e) {}

      delete content.midiAccessRequestPromise;
      return false;
    }
  );
  ok(accessGranted, "requestMIDIAccess resolved");

  assertSitePermissionInstallTelemetryEvents([]);
});

add_task(async function testDisabledRequestMIDIAccessFile() {
  let dir = getChromeDir(getResolvedURI(gTestPath));
  dir.append("blank.html");
  const fileSchemeTestUri = Services.io.newFileURI(dir).spec;

  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, fileSchemeTestUri);
  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);

  info("Check that requestMIDIAccess isn't set on navigator on file scheme");
  const isRequestMIDIAccessDefined = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    () => {
      return "requestMIDIAccess" in content.wrappedJSObject.navigator;
    }
  );
  is(
    isRequestMIDIAccessDefined,
    false,
    "navigator.requestMIDIAccess is not defined on file scheme"
  );
});

// Ignore any additional telemetry events collected in this file.
// Unfortunately it doesn't work to have this in a cleanup function.
// Keep this as the last task done.
add_task(function teardown_telemetry_events() {
  AddonTestUtils.getAMTelemetryEvents();
});

/**
 *  Check that the expected sitepermission install events are recorded.
 *
 * @param {Array<String>} expectedSteps: An array of the expected extra.step values recorded.
 */

function assertSitePermissionInstallTelemetryEvents(expectedSteps) {
  let amInstallEvents = AddonTestUtils.getAMTelemetryEvents()
    .filter(evt => evt.method === "install" && evt.object === "sitepermission")
    .map(evt => evt.extra.step);

  Assert.deepEqual(amInstallEvents, expectedSteps);
}

async function waitForInstallDialog(id = "addon-webext-permissions") {
  let panel = await waitForNotification(id);
  return panel.childNodes[0];
}

/**
 * Adds an event listener that will listen for post-install dialog event and automatically
 * close the dialogs.
 */

function alwaysAcceptAddonPostInstallDialogs() {
  // Once the addon is installed, a dialog is displayed as a confirmation.
  // This could interfere with tests running after this one, so we set up a listener
  // that will always accept post install dialogs so we don't have  to deal with them in
  // the test.
  const abortController = new AbortController();

  const { AppMenuNotifications } = ChromeUtils.importESModule(
    "resource://gre/modules/AppMenuNotifications.sys.mjs"
  );
  info("Start listening and accept addon post-install notifications");
  PanelUI.notificationPanel.addEventListener(
    "popupshown",
    async function popupshown() {
      let notification = AppMenuNotifications.activeNotification;
      if (!notification || notification.id !== "addon-installed") {
        return;
      }

      let popupnotificationID = PanelUI._getPopupId(notification);
      if (popupnotificationID) {
        info("Accept post-install dialog");
        let popupnotification = document.getElementById(popupnotificationID);
        popupnotification?.button.click();
      }
    },
    {
      signal: abortController.signal,
    }
  );

  registerCleanupFunction(async () => {
    // Clear the listener at the end of the test file, to prevent it to stay
    // around when the same browser instance may be running other unrelated
    // test files.
    abortController.abort();
  });
}

const PROGRESS_NOTIFICATION = "addon-progress";
async function waitForNotification(notificationId) {
  info(`Waiting for ${notificationId} notification`);

  let topic = getObserverTopic(notificationId);

  let observerPromise;
  if (notificationId !== "addon-webext-permissions") {
    observerPromise = new Promise(resolve => {
      Services.obs.addObserver(function observer(aSubject, aTopic) {
        // Ignore the progress notification unless that is the notification we want
        if (
          notificationId != PROGRESS_NOTIFICATION &&
          aTopic == getObserverTopic(PROGRESS_NOTIFICATION)
        ) {
          return;
        }
        Services.obs.removeObserver(observer, topic);
        resolve();
      }, topic);
    });
  }

  let panelEventPromise = new Promise(resolve => {
    window.PopupNotifications.panel.addEventListener(
      "PanelUpdated",
      function eventListener(e) {
        // Skip notifications that are not the one that we are supposed to be looking for
        if (!e.detail.includes(notificationId)) {
          return;
        }
        window.PopupNotifications.panel.removeEventListener(
          "PanelUpdated",
          eventListener
        );
        resolve();
      }
    );
  });

  await observerPromise;
  await panelEventPromise;
  await waitForTick();

  info(`Saw a ${notificationId} notification`);
  await SimpleTest.promiseFocus(window.PopupNotifications.window);
  return window.PopupNotifications.panel;
}

// This function is similar to the one in
// toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js,
// please keep both in sync!
function getObserverTopic(aNotificationId) {
  let topic = aNotificationId;
  if (topic == "xpinstall-disabled") {
    topic = "addon-install-disabled";
  } else if (topic == "addon-progress") {
    topic = "addon-install-started";
  } else if (topic == "addon-installed") {
    topic = "webextension-install-notify";
  }
  return topic;
}

function waitForTick() {
  return new Promise(resolve => executeSoon(resolve));
}

Messung V0.5
C=94 H=97 G=95

¤ Dauer der Verarbeitung: 0.17 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.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge