<!--
  Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/
-->
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <title>Test for Permissions API</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" href="/tests/SimpleTest/test.css">
</head>

<body>
  <pre id="test"></pre>
  <script type="application/javascript">
  /*globals SpecialPowers, SimpleTest, is, ok, */
  'use strict';

  const {
    UNKNOWN_ACTION,
    PROMPT_ACTION,
    ALLOW_ACTION,
    DENY_ACTION
  } = SpecialPowers.Ci.nsIPermissionManager;

  SimpleTest.waitForExplicitFinish();

  const PERMISSIONS = [{
    name: 'geolocation',
    type: 'geo'
  }, {
    name: 'notifications',
    type: 'desktop-notification'
  }, {
    name: 'push',
    type: 'desktop-notification'
  }, {
    name: 'persistent-storage',
    type: 'persistent-storage'
  }, {
    name: 'midi',
    type: 'midi'
  }, ];

  const UNSUPPORTED_PERMISSIONS = [
    'foobarbaz', // Not in spec, for testing only.
  ];

  // Create a closure, so that tests are run on the correct window object.
  function createPermissionTester(iframe) {
    const iframeWindow = iframe.contentWindow;
    return {
      async setPermissions(allow, context = iframeWindow.document) {
        const permissions = PERMISSIONS.map(({ type }) => {
          return {
            type,
            allow,
            context,
          };
        });
        await SpecialPowers.popPermissions();
        return SpecialPowers.pushPermissions(permissions);
      },
      revokePermissions() {
        const promisesToRevoke = PERMISSIONS.map(({ name })  => {
          return iframeWindow.navigator.permissions
            .revoke({ name })
            .then(
              ({ state }) => is(state, 'prompt', `correct state for '${name}'`),
              () => ok(false, `revoke should not have rejected for '${name}'`)
            );
        });
        return Promise.all(promisesToRevoke);
      },
      revokeUnsupportedPermissions() {
        const promisesToRevoke = UNSUPPORTED_PERMISSIONS.map(({ name }) => {
          return iframeWindow.navigator.permissions
            .revoke({ name })
            .then(
              () => ok(false, `revoke should not have resolved for '${name}'`),
              error => is(error.name, 'TypeError', `revoke should have thrown TypeError for '${name}'`)
            );
        });
        return Promise.all(promisesToRevoke);
      },
      checkPermissions(expectedState) {
        const promisesToQuery = PERMISSIONS.map(({ name: expectedName }) => {
          return iframeWindow.navigator.permissions
            .query({ name: expectedName })
            .then(
              ({ state, name }) => {
                is(state, expectedState, `correct state for '${expectedName}'`)
                is(name, expectedName, `correct name for '${expectedName}'`)
              },
              () => ok(false, `query should not have rejected for '${name}'`)
            );
          });
        return Promise.all(promisesToQuery);
      },
      checkUnsupportedPermissions() {
        const promisesToQuery = UNSUPPORTED_PERMISSIONS.map(({ name }) => {
          return iframeWindow.navigator.permissions
            .query({ name })
            .then(
              () => ok(false, `query should not have resolved for '${name}'`),
              error => {
                is(error.name, 'TypeError',
                  `query should have thrown TypeError for '${name}'`);
              }
            );
          });
        return Promise.all(promisesToQuery);
      },
      promiseStateChanged(name, state) {
        return iframeWindow.navigator.permissions
          .query({ name })
          .then(status => {
            return new Promise( resolve => {
              status.onchange = () => {
                status.onchange = null;
                is(status.state, state, `state changed for '${name}'`);
                resolve();
              };
            });
          },
          () => ok(false, `query should not have rejected for '${name}'`));
      },
      testStatusOnChange() {
        return new Promise((resolve) => {
          SpecialPowers.popPermissions(() => {
            const permission = 'geolocation';
            const promiseGranted = this.promiseStateChanged(permission, 'granted');
            this.setPermissions(ALLOW_ACTION);
            promiseGranted.then(async () => {
              const promisePrompt = this.promiseStateChanged(permission, 'prompt');
              await SpecialPowers.popPermissions();
              return promisePrompt;
            }).then(resolve);
          });
        });
      },
      testInvalidQuery() {
        return iframeWindow.navigator.permissions
          .query({ name: 'invalid' })
          .then(
            () => ok(false, 'invalid query should not have resolved'),
            () => ok(true, 'invalid query should have rejected')
          );
      },
      testInvalidRevoke() {
        return iframeWindow.navigator.permissions
          .revoke({ name: 'invalid' })
          .then(
            () => ok(false, 'invalid revoke should not have resolved'),
            () => ok(true, 'invalid revoke should have rejected')
          );
      },
      async testNotFullyActiveDoc() {
        const iframe1 = await createIframe();
        const expectedErrorClass = iframe1.contentWindow.DOMException;
        const permAPI = iframe1.contentWindow.navigator.permissions;
        // Document no longer fully active
        iframe1.remove();
        await new Promise((res) => {
          permAPI.query({ name: "geolocation" }).catch((error) => {
            ok(
              error instanceof expectedErrorClass,
              "DOMException from other realm"
            );
            is(
              error.name,
              "InvalidStateError",
              "Must reject with a InvalidStateError"
            );
            iframe1.remove();
            res();
          });
        });
      },
      async testNotFullyActiveChange() {
        await SpecialPowers.popPermissions();
        const iframe2 = await createIframe();
        const initialStatus = await iframe2.contentWindow.navigator.permissions.query(
          { name: "geolocation" }
        );
        await SpecialPowers.pushPermissions([
          {
            type: "geo",
            allow: PROMPT_ACTION,
            context: iframe2.contentWindow.document,
          },
        ]);
        is(
          initialStatus.state,
          "prompt",
          "Initially the iframe's permission is prompt"
        );

        // Document no longer fully active
        const stolenDoc = iframe2.contentWindow.document;
        iframe2.remove();
        initialStatus.onchange = () => {
          ok(false, "onchange must not fire when document is not fully active.");
        };
        // We set it to grant for this origin, but the PermissionStatus doesn't change.
        await SpecialPowers.pushPermissions([
          {
            type: "geo",
            allow: ALLOW_ACTION,
            context: stolenDoc,
          },
        ]);
        is(
          initialStatus.state,
          "prompt",
          "Inactive document's permission must not change"
        );

        // Re-attach the iframe
        document.body.appendChild(iframe2);
        await new Promise((res) => (iframe2.onload = res));
        // Fully active again
        const newStatus = await iframe2.contentWindow.navigator.permissions.query({
          name: "geolocation",
        });
        is(newStatus.state, "granted", "Reflect that we are granted");

        const newEventPromise = new Promise((res) => (newStatus.onchange = res));
        await SpecialPowers.pushPermissions([
          {
            type: "geo",
            allow: DENY_ACTION,
            context: iframe2.contentWindow.document,
          },
        ]);
        // Event fires...
        await newEventPromise;
        is(initialStatus.state, "prompt", "Remains prompt, as it's actually dead.");
        is(newStatus.state, "denied", "New status must be 'denied'.");
        iframe2.remove();
      },
    };
  }

  function enablePrefs() {
    const ops = {
      'set': [
        ['dom.permissions.revoke.enable', true],
      ],
    };
    return SpecialPowers.pushPrefEnv(ops);
  }

  function createIframe() {
    return new Promise((resolve) => {
      const iframe = document.createElement('iframe');
      iframe.src = 'file_empty.html';
      iframe.onload = () => resolve(iframe);
      document.body.appendChild(iframe);
    });
  }

  window.onload = () => {
    enablePrefs()
      .then(createIframe)
      .then(createPermissionTester)
      .then((tester) => {
        return tester
          .checkUnsupportedPermissions()
          .then(() => tester.setPermissions(UNKNOWN_ACTION))
          .then(() => tester.checkPermissions('prompt'))
          .then(() => tester.setPermissions(PROMPT_ACTION))
          .then(() => tester.checkPermissions('prompt'))
          .then(() => tester.setPermissions(ALLOW_ACTION))
          .then(() => tester.checkPermissions('granted'))
          .then(() => tester.setPermissions(DENY_ACTION))
          .then(() => tester.checkPermissions('denied'))
          .then(() => tester.testStatusOnChange())
          .then(() => tester.testInvalidQuery())
          .then(() => tester.revokeUnsupportedPermissions())
          .then(() => tester.revokePermissions())
          .then(() => tester.checkPermissions('prompt'))
          .then(() => tester.testInvalidRevoke())
          .then(() => tester.testNotFullyActiveDoc())
          .then(() => tester.testNotFullyActiveChange());
      })
      .then(SimpleTest.finish)
      .catch((e) => {
        ok(false, `Unexpected error ${e}`);
        SimpleTest.finish();
      });
  };
  </script>
</body>

</html>
