<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Tabs Events Test</title>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
  <script type="text/javascript" src="head.js"></script>
  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>

<script type="text/javascript">
"use strict";

add_task(async function testTabEvents() {
  async function background() {
    const events = [];
    let eventPromise;
    const checkEvents = () => {
      if (eventPromise && events.length >= eventPromise.names.length) {
        eventPromise.resolve();
      }
    };

    browser.tabs.onCreated.addListener(tab => {
      events.push({type: "onCreated", tab});
      checkEvents();
    });

    browser.tabs.onAttached.addListener((tabId, info) => {
      events.push(Object.assign({type: "onAttached", tabId}, info));
      checkEvents();
    });

    browser.tabs.onDetached.addListener((tabId, info) => {
      events.push(Object.assign({type: "onDetached", tabId}, info));
      checkEvents();
    });

    browser.tabs.onRemoved.addListener((tabId, info) => {
      events.push(Object.assign({type: "onRemoved", tabId}, info));
      checkEvents();
    });

    browser.tabs.onMoved.addListener((tabId, info) => {
      events.push(Object.assign({type: "onMoved", tabId}, info));
      checkEvents();
    });

    async function expectEvents(names) {
      browser.test.log(`Expecting events: ${names.join(", ")}`);

      await new Promise(resolve => {
        eventPromise = {names, resolve};
        checkEvents();
      });

      browser.test.assertEq(names.length, events.length, "Got expected number of events");
      for (const [i, name] of names.entries()) {
        browser.test.assertEq(name, i in events && events[i].type,
                              `Got expected ${name} event`);
      }
      return events.splice(0);
    }

    try {
      browser.test.log("Create tab");
      const tab = await browser.tabs.create({url: "about:blank"});
      const oldIndex = tab.index;

      const [created] = await expectEvents(["onCreated"]);
      browser.test.assertEq(tab.id, created.tab.id, "Got expected tab ID");
      browser.test.assertEq(oldIndex, created.tab.index, "Got expected tab index");


      browser.test.log("Remove tab");
      await browser.tabs.remove(tab.id);
      const [removed] = await expectEvents(["onRemoved"]);

      browser.test.assertEq(tab.id, removed.tabId, "Expected removed tab ID");
      browser.test.assertEq(tab.windowId, removed.windowId, "Expected removed tab window ID");
      // Note: We want to test for the actual boolean value false here.
      browser.test.assertEq(false, removed.isWindowClosing, "Expected isWindowClosing value");

      browser.test.notifyPass("tabs-events");
    } catch (e) {
      browser.test.fail(`${e} :: ${e.stack}`);
      browser.test.notifyFail("tabs-events");
    }
  }

  const extension = ExtensionTestUtils.loadExtension({
    manifest: {
      "permissions": ["tabs"],
    },
    background,
  });

  await extension.startup();
  await extension.awaitFinish("tabs-events");
  await extension.unload();
});

add_task(async function testTabRemovalEvent() {
  async function background() {
    function awaitLoad(tabId) {
      return new Promise(resolve => {
        browser.tabs.onUpdated.addListener(function listener(tabId_, changed, tab) {
          if (tabId == tabId_ && changed.status == "complete") {
            browser.tabs.onUpdated.removeListener(listener);
            resolve();
          }
        });
      });
    }

    chrome.tabs.onRemoved.addListener((tabId, info) => {
      browser.test.log("Make sure the removed tab is not available in the tabs.query callback.");
      chrome.tabs.query({}, tabs => {
        for (const tab of tabs) {
          browser.test.assertTrue(tab.id != tabId, "Tab query should not include removed tabId");
        }
        browser.test.notifyPass("tabs-events");
      });
    });

    try {
      const url = "http://example.com/mochitest/mobile/android/components/extensions/test/mochitest/context.html";
      const tab = await browser.tabs.create({url: url});
      await awaitLoad(tab.id);

      await browser.tabs.remove(tab.id);
    } catch (e) {
      browser.test.fail(`${e} :: ${e.stack}`);
      browser.test.notifyFail("tabs-events");
    }
  }

  const extension = ExtensionTestUtils.loadExtension({
    manifest: {
      "permissions": ["tabs"],
    },
    background,
  });

  await extension.startup();
  await extension.awaitFinish("tabs-events");
  await extension.unload();
});

add_task(async function testTabActivationEvent() {
  // TODO bug 1565536: tabs.onActivated is not supported in GeckoView.
  if (true) {
    todo(false, "skipping testTabActivationEvent");
    return;
  }
  async function background() {
    function makeExpectable() {
      let expectation = null, resolver = null;
      const expectable = param => {
        if (expectation === null) {
          browser.test.fail("unexpected call to expectable");
        } else {
          try {
            resolver(expectation(param));
          } catch (e) {
            resolver(Promise.reject(e));
          } finally {
            expectation = null;
          }
        }
      };
      expectable.expect = e => {
        expectation = e;
        return new Promise(r => { resolver = r; });
      };
      return expectable;
    }
    try {
      const listener = makeExpectable();
      browser.tabs.onActivated.addListener(listener);

      const [tab0] = await browser.tabs.query({active: true});
      const [, tab1] = await Promise.all([
        listener.expect(info => {
          browser.test.assertEq(tab0.id, info.previousTabId, "Got expected previousTabId");
        }),
        browser.tabs.create({url: "about:blank"}),
      ]);
      const [, tab2] = await Promise.all([
        listener.expect(info => {
          browser.test.assertEq(tab1.id, info.previousTabId, "Got expected previousTabId");
        }),
        browser.tabs.create({url: "about:blank"}),
      ]);

      await Promise.all([
        listener.expect(info => {
          browser.test.assertEq(tab1.id, info.tabId, "Got expected tabId");
          browser.test.assertEq(tab2.id, info.previousTabId, "Got expected previousTabId");
        }),
        browser.tabs.update(tab1.id, {active: true}),
      ]);

      await Promise.all([
        listener.expect(info => {
          browser.test.assertEq(tab2.id, info.tabId, "Got expected tabId");
          browser.test.assertEq(undefined, info.previousTabId, "previousTabId should not be defined when previous tab was closed");
        }),
        browser.tabs.remove(tab1.id),
      ]);

      browser.tabs.onActivated.removeListener(listener);
      await browser.tabs.remove(tab2.id);

      browser.test.notifyPass("tabs-events");
    } catch (e) {
      browser.test.fail(`${e} :: ${e.stack}`);
      browser.test.notifyFail("tabs-events");
    }
  }

  const extension = ExtensionTestUtils.loadExtension({
    manifest: {
      "permissions": ["tabs"],
    },

    background,
  });

  await extension.startup();
  await extension.awaitFinish("tabs-events");
  await extension.unload();
});

add_task(async function test_tabs_event_page() {
  function background() {
    const EVENTS = [
      "onActivated",
      "onRemoved",
      "onUpdated",
    ];
    browser.tabs.onCreated.addListener(() => {
      browser.test.sendMessage("onCreated");
    });
    for (const event of EVENTS) {
      browser.tabs[event].addListener(() => {
      });
    }
    browser.test.onMessage.addListener(async msg => {
      if (msg === "createTab") {
        await browser.tabs.create({url: "about:blank"});
      }
    });
    browser.test.sendMessage("ready");
  }

  const apiEvents = ["onActivated", "onCreated", "onRemoved", "onUpdated"];
  const apiNs = "tabs";
  const extension = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: { gecko: { id: "eventpage@tabs" } },
      "permissions": ["tabs"],
      background: { persistent: false },
    },
    background,
  });

  await extension.startup();
  info("Wait for event page to be started");
  await extension.awaitMessage("ready");
  // Sanity check
  info("Wait for tabs.onCreated listener call");
  extension.sendMessage("createTab");
  await extension.awaitMessage("onCreated");

  // on startup, all event listeners should not be primed
  await assertPersistentListeners(extension, apiNs, apiEvents, { primed: false });

  // when the extension is killed, all event listeners should be primed
  info("Terminate event page");
  await extension.terminateBackground({ disableResetIdleForTest: true });
  await assertPersistentListeners(extension, apiNs, apiEvents, { primed: true });

  // on start up again, all event listeners should not be primed
  info("Wake up event page on a new tabs.onCreated event");
  const newWin = window.open();
  info("Wait for event page to be restarted");
  await extension.awaitMessage("ready");
  info("Wait for the primed tabs.onCreated to be received by the event page");
  await extension.awaitMessage("onCreated");
  await assertPersistentListeners(extension, apiNs, apiEvents, { primed: false });

  await extension.unload();
  newWin.close();
});

</script>

</body>
</html>
