"use strict";

const {sinon} = ChromeUtils.import("resource://testing-common/Sinon.jsm");
/* global UIState */

const lastModifiedFixture = 1507655615.87; // Approx Oct 10th 2017
const mockTargets = [
  { id: "0", name: "foo", type: "phone", clientRecord: {id: "cli0", serverLastModified: lastModifiedFixture, type: "phone"} },
  { id: "1", name: "bar", type: "desktop", clientRecord: {id: "cli1", serverLastModified: lastModifiedFixture, type: "desktop"} },
  { id: "2", name: "baz", type: "phone", clientRecord: {id: "cli2", serverLastModified: lastModifiedFixture, type: "phone"} },
  { id: "3", name: "no client record device", type: "phone" },
];

add_task(async function bookmark() {
  // Open a unique page.
  let url = "http://example.com/browser_page_action_menu";
  await BrowserTestUtils.withNewTab(url, async () => {
    // Open the panel.
    await promisePageActionPanelOpen();

    // The bookmark button should read "Bookmark This Page" and not be starred.
    let bookmarkButton = document.getElementById("pageAction-panel-bookmark");
    Assert.equal(bookmarkButton.label, "Bookmark This Page");
    Assert.ok(!bookmarkButton.hasAttribute("starred"));

    // Click the button.
    let hiddenPromise = promisePageActionPanelHidden();
    EventUtils.synthesizeMouseAtCenter(bookmarkButton, {});
    await hiddenPromise;

    // Make sure the edit-bookmark panel opens, then hide it.
    await new Promise(resolve => {
      if (StarUI.panel.state == "open") {
        resolve();
        return;
      }
      StarUI.panel.addEventListener("popupshown", resolve, { once: true });
    });
    Assert.equal(BookmarkingUI.starBox.getAttribute("open"), "true",
      "Star has open attribute");
    StarUI.panel.hidePopup();
    Assert.ok(!BookmarkingUI.starBox.hasAttribute("open"),
      "Star no longer has open attribute");

    // Open the panel again.
    await promisePageActionPanelOpen();

    // The bookmark button should now read "Edit This Bookmark" and be starred.
    Assert.equal(bookmarkButton.label, "Edit This Bookmark");
    Assert.ok(bookmarkButton.hasAttribute("starred"));
    Assert.equal(bookmarkButton.getAttribute("starred"), "true");

    // Click it again.
    hiddenPromise = promisePageActionPanelHidden();
    EventUtils.synthesizeMouseAtCenter(bookmarkButton, {});
    await hiddenPromise;

    // The edit-bookmark panel should open again.
    await new Promise(resolve => {
      if (StarUI.panel.state == "open") {
        resolve();
        return;
      }
      StarUI.panel.addEventListener("popupshown", resolve, { once: true });
    });

    let onItemRemovedPromise = PlacesTestUtils.waitForNotification("onItemRemoved",
      (id, parentId, index, type, itemUrl) => url == itemUrl.spec);

    // Click the remove-bookmark button in the panel.
    StarUI._element("editBookmarkPanelRemoveButton").click();

    // Wait for the bookmark to be removed before continuing.
    await onItemRemovedPromise;

    // Open the panel again.
    await promisePageActionPanelOpen();

    // The bookmark button should read "Bookmark This Page" and not be starred.
    Assert.equal(bookmarkButton.label, "Bookmark This Page");
    Assert.ok(!bookmarkButton.hasAttribute("starred"));

    // Done.
    hiddenPromise = promisePageActionPanelHidden();
    BrowserPageActions.panelNode.hidePopup();
    await hiddenPromise;
  });
});

add_task(async function pinTabFromPanel() {
  // Open an actionable page so that the main page action button appears.  (It
  // does not appear on about:blank for example.)
  let url = "http://example.com/";
  await BrowserTestUtils.withNewTab(url, async () => {
    // Open the panel and click Pin Tab.
    await promisePageActionPanelOpen();

    let pinTabButton = document.getElementById("pageAction-panel-pinTab");
    Assert.equal(pinTabButton.label, "Pin Tab");
    let hiddenPromise = promisePageActionPanelHidden();
    EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
    await hiddenPromise;

    Assert.ok(gBrowser.selectedTab.pinned, "Tab was pinned");

    // Open the panel and click Unpin Tab.
    Assert.equal(pinTabButton.label, "Unpin Tab");
    await promisePageActionPanelOpen();

    hiddenPromise = promisePageActionPanelHidden();
    EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
    await hiddenPromise;

    Assert.ok(!gBrowser.selectedTab.pinned, "Tab was unpinned");
  });
});


add_task(async function pinTabFromURLBar() {
  // Open an actionable page so that the main page action button appears.  (It
  // does not appear on about:blank for example.)
  let url = "http://example.com/";
  await BrowserTestUtils.withNewTab(url, async () => {
    // Add action to URL bar.
    let action = PageActions._builtInActions.find(a => a.id == "pinTab");
    action.pinnedToUrlbar = true;
    registerCleanupFunction(() => action.pinnedToUrlbar = false);

    // Click the Pin Tab button.
    let pinTabButton = document.getElementById("pageAction-urlbar-pinTab");
    EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
    await BrowserTestUtils.waitForCondition(() => gBrowser.selectedTab.pinned,
      "Tab was pinned");

    // Click the Unpin Tab button
    EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
    await BrowserTestUtils.waitForCondition(() => !gBrowser.selectedTab.pinned,
      "Tab was unpinned");
  });
});

add_task(async function emailLink() {
  // Open an actionable page so that the main page action button appears.  (It
  // does not appear on about:blank for example.)
  let url = "http://example.com/";
  await BrowserTestUtils.withNewTab(url, async () => {
    // Replace the email-link entry point to check whether it's called.
    let originalFn = MailIntegration.sendLinkForBrowser;
    let fnCalled = false;
    MailIntegration.sendLinkForBrowser = () => {
      fnCalled = true;
    };
    registerCleanupFunction(() => {
      MailIntegration.sendLinkForBrowser = originalFn;
    });

    // Open the panel and click Email Link.
    await promisePageActionPanelOpen();
    let emailLinkButton =
      document.getElementById("pageAction-panel-emailLink");
    let hiddenPromise = promisePageActionPanelHidden();
    EventUtils.synthesizeMouseAtCenter(emailLinkButton, {});
    await hiddenPromise;

    Assert.ok(fnCalled);
  });
});

add_task(async function copyURLFromPanel() {
  // Open an actionable page so that the main page action button appears.  (It
  // does not appear on about:blank for example.)
  let url = "http://example.com/";
  await BrowserTestUtils.withNewTab(url, async () => {
    // Add action to URL bar.
    let action = PageActions._builtInActions.find(a => a.id == "copyURL");
    action.pinnedToUrlbar = true;
    registerCleanupFunction(() => action.pinnedToUrlbar = false);

    // Open the panel and click Copy URL.
    await promisePageActionPanelOpen();
    Assert.ok(true, "page action panel opened");

    let copyURLButton =
      document.getElementById("pageAction-panel-copyURL");
    let hiddenPromise = promisePageActionPanelHidden();
    EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
    await hiddenPromise;

    let feedbackPanel = document.getElementById("confirmation-hint");
    let feedbackShownPromise = BrowserTestUtils.waitForEvent(feedbackPanel, "popupshown");
    await feedbackShownPromise;
    Assert.equal(feedbackPanel.anchorNode.id, "pageActionButton", "Feedback menu should be anchored on the main Page Action button");
    let feedbackHiddenPromise = promisePanelHidden("confirmation-hint");
    await feedbackHiddenPromise;

    action.pinnedToUrlbar = false;
  });
});

add_task(async function copyURLFromURLBar() {
  // Open an actionable page so that the main page action button appears.  (It
  // does not appear on about:blank for example.)
  let url = "http://example.com/";
  await BrowserTestUtils.withNewTab(url, async () => {
    // Add action to URL bar.
    let action = PageActions._builtInActions.find(a => a.id == "copyURL");
    action.pinnedToUrlbar = true;
    registerCleanupFunction(() => action.pinnedToUrlbar = false);

    let copyURLButton =
      document.getElementById("pageAction-urlbar-copyURL");
    let feedbackShownPromise = promisePanelShown("confirmation-hint");
    EventUtils.synthesizeMouseAtCenter(copyURLButton, {});

    await feedbackShownPromise;
    let panel = document.getElementById("confirmation-hint");
    Assert.equal(panel.anchorNode.id, "pageAction-urlbar-copyURL", "Feedback menu should be anchored on the main URL bar button");
    let feedbackHiddenPromise = promisePanelHidden("confirmation-hint");
    await feedbackHiddenPromise;

    action.pinnedToUrlbar = false;
  });
});

add_task(async function sendToDevice_nonSendable() {
  // Open a tab that's not sendable but where the page action buttons still
  // appear.  about:about is convenient.
  await BrowserTestUtils.withNewTab("about:about", async () => {
    await promiseSyncReady();
    // Open the panel.  Send to Device should be disabled.
    await promisePageActionPanelOpen();
    Assert.equal(BrowserPageActions.mainButtonNode.getAttribute("open"), "true",
                 "Main button has 'open' attribute");
    let panelButton =
      BrowserPageActions.panelButtonNodeForActionID("sendToDevice");
    Assert.equal(panelButton.disabled, true,
                 "The panel button should be disabled");
    let hiddenPromise = promisePageActionPanelHidden();
    BrowserPageActions.panelNode.hidePopup();
    await hiddenPromise;
    Assert.ok(!BrowserPageActions.mainButtonNode.hasAttribute("open"),
              "Main button no longer has 'open' attribute");
    // The urlbar button shouldn't exist.
    let urlbarButton =
      BrowserPageActions.urlbarButtonNodeForActionID("sendToDevice");
    Assert.equal(urlbarButton, null, "The urlbar button shouldn't exist");
  });
});

add_task(async function sendToDevice_syncNotReady_other_states() {
  // Open a tab that's sendable.
  await BrowserTestUtils.withNewTab("http://example.com/", async () => {
    await promiseSyncReady();
    const sandbox = sinon.createSandbox();
    sandbox.stub(gSync, "syncReady").get(() => false);
    sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_NOT_VERIFIED });
    sandbox.stub(gSync, "isSendableURI").returns(true);

    let cleanUp = () => {
      sandbox.restore();
    };
    registerCleanupFunction(cleanUp);

    // Open the panel.
    await promisePageActionPanelOpen();
    let sendToDeviceButton =
      document.getElementById("pageAction-panel-sendToDevice");
    Assert.ok(!sendToDeviceButton.disabled);

    // Click Send to Device.
    let viewPromise = promisePageActionViewShown();
    EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
    let view = await viewPromise;
    Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");

    let expectedItems = [
      {
        className: "pageAction-sendToDevice-notReady",
        display: "none",
        disabled: true,
      },
      {
        attrs: {
          label: "Account Not Verified",
        },
        disabled: true,
      },
      null,
      {
        attrs: {
          label: "Verify Your Account...",
        },
      },
    ];
    checkSendToDeviceItems(expectedItems);

    // Done, hide the panel.
    let hiddenPromise = promisePageActionPanelHidden();
    BrowserPageActions.panelNode.hidePopup();
    await hiddenPromise;

    cleanUp();
  });
});

add_task(async function sendToDevice_syncNotReady_configured() {
  // Open a tab that's sendable.
  await BrowserTestUtils.withNewTab("http://example.com/", async () => {
    await promiseSyncReady();
    const sandbox = sinon.createSandbox();
    const syncReady = sandbox.stub(gSync, "syncReady").get(() => false);
    const hasSyncedThisSession = sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => false);
    sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
    sandbox.stub(gSync, "isSendableURI").returns(true);

    sandbox.stub(Weave.Service, "sync").callsFake(() => {
      syncReady.get(() => true);
      hasSyncedThisSession.get(() => true);
      sandbox.stub(gSync, "sendTabTargets").get(() => mockTargets);
      sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type);
    });

    let onShowingSubview = BrowserPageActions.sendToDevice.onShowingSubview;
    sandbox.stub(BrowserPageActions.sendToDevice, "onShowingSubview").callsFake((...args) => {
      this.numCall++ || (this.numCall = 1);
      onShowingSubview.call(BrowserPageActions.sendToDevice, ...args);
      testSendTabToDeviceMenu(this.numCall);
    });

    let cleanUp = () => {
      sandbox.restore();
    };
    registerCleanupFunction(cleanUp);

    // Open the panel.
    await promisePageActionPanelOpen();
    let sendToDeviceButton =
      document.getElementById("pageAction-panel-sendToDevice");
    Assert.ok(!sendToDeviceButton.disabled);

    // Click Send to Device.
    let viewPromise = promisePageActionViewShown();
    EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
    let view = await viewPromise;
    Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");

    function testSendTabToDeviceMenu(numCall) {
      if (numCall == 1) {
        // "Syncing devices" should be shown.
        checkSendToDeviceItems([
          {
            className: "pageAction-sendToDevice-notReady",
            disabled: true,
          },
        ]);
      } else if (numCall == 2) {
        // The devices should be shown in the subview.
        let expectedItems = [
          {
            className: "pageAction-sendToDevice-notReady",
            display: "none",
            disabled: true,
          },
        ];
        for (let target of mockTargets) {
          const attrs = {
            clientId: target.id,
            label: target.name,
            clientType: target.type,
          };
          if (target.clientRecord && target.clientRecord.serverLastModified) {
            attrs.tooltiptext = gSync.formatLastSyncDate(new Date(target.clientRecord.serverLastModified * 1000));
          }
          expectedItems.push({
            attrs,
          });
        }
        expectedItems.push(
          null,
          {
            attrs: {
              label: "Send to All Devices",
            },
          }
        );
        checkSendToDeviceItems(expectedItems);
      } else {
        ok(false, "This should never happen");
      }
    }

    // Done, hide the panel.
    let hiddenPromise = promisePageActionPanelHidden();
    BrowserPageActions.panelNode.hidePopup();
    await hiddenPromise;
    cleanUp();
  });
});

add_task(async function sendToDevice_notSignedIn() {
  // Open a tab that's sendable.
  await BrowserTestUtils.withNewTab("http://example.com/", async () => {
    await promiseSyncReady();

    // Open the panel.
    await promisePageActionPanelOpen();
    let sendToDeviceButton =
      document.getElementById("pageAction-panel-sendToDevice");
    Assert.ok(!sendToDeviceButton.disabled);

    // Click Send to Device.
    let viewPromise = promisePageActionViewShown();
    EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
    let view = await viewPromise;
    Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");

    let expectedItems = [
      {
        className: "pageAction-sendToDevice-notReady",
        display: "none",
        disabled: true,
      },
      {
        attrs: {
          label: "Not Connected to Sync",
        },
        disabled: true,
      },
      null,
      {
        attrs: {
          label: "Sign in to Sync...",
        },
      },
      {
        attrs: {
          label: "Learn About Sending Tabs...",
        },
      },
    ];
    checkSendToDeviceItems(expectedItems);

    // Done, hide the panel.
    let hiddenPromise = promisePageActionPanelHidden();
    BrowserPageActions.panelNode.hidePopup();
    await hiddenPromise;
  });
});

add_task(async function sendToDevice_noDevices() {
  // Open a tab that's sendable.
  await BrowserTestUtils.withNewTab("http://example.com/", async () => {
    await promiseSyncReady();
    const sandbox = sinon.createSandbox();
    sandbox.stub(gSync, "syncReady").get(() => true);
    sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => true);
    sandbox.stub(Weave.Service.clientsEngine, "fxaDevices").get(() => []);
    sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
    sandbox.stub(gSync, "isSendableURI").returns(true);
    sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type);

    let cleanUp = () => {
      sandbox.restore();
    };
    registerCleanupFunction(cleanUp);

    // Open the panel.
    await promisePageActionPanelOpen();
    let sendToDeviceButton =
      document.getElementById("pageAction-panel-sendToDevice");
    Assert.ok(!sendToDeviceButton.disabled);

    // Click Send to Device.
    let viewPromise = promisePageActionViewShown();
    EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
    let view = await viewPromise;
    Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");

    let expectedItems = [
      {
        className: "pageAction-sendToDevice-notReady",
        display: "none",
        disabled: true,
      },
      {
        attrs: {
          label: "No Devices Connected",
        },
        disabled: true,
      },
      null,
      {
        attrs: {
          label: "Connect Another Device...",
        },
      },
      {
        attrs: {
          label: "Learn About Sending Tabs...",
        },
      },
    ];
    checkSendToDeviceItems(expectedItems);

    // Done, hide the panel.
    let hiddenPromise = promisePageActionPanelHidden();
    BrowserPageActions.panelNode.hidePopup();
    await hiddenPromise;

    cleanUp();

    await UIState.reset();
  });
});

add_task(async function sendToDevice_devices() {
  // Open a tab that's sendable.
  await BrowserTestUtils.withNewTab("http://example.com/", async () => {
    await promiseSyncReady();
    const sandbox = sinon.createSandbox();
    sandbox.stub(gSync, "syncReady").get(() => true);
    sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => true);
    sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
    sandbox.stub(gSync, "isSendableURI").returns(true);
    sandbox.stub(gSync, "sendTabTargets").get(() => mockTargets);
    sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type);

    let cleanUp = () => {
      sandbox.restore();
    };
    registerCleanupFunction(cleanUp);

    // Open the panel.
    await promisePageActionPanelOpen();
    let sendToDeviceButton =
      document.getElementById("pageAction-panel-sendToDevice");
    Assert.ok(!sendToDeviceButton.disabled);

    // Click Send to Device.
    let viewPromise = promisePageActionViewShown();
    EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
    let view = await viewPromise;
    Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");

    // The devices should be shown in the subview.
    let expectedItems = [
      {
        className: "pageAction-sendToDevice-notReady",
        display: "none",
        disabled: true,
      },
    ];
    for (let target of mockTargets) {
      expectedItems.push({
        attrs: {
          clientId: target.id,
          label: target.name,
          clientType: target.type,
        },
      });
    }
    expectedItems.push(
      null,
      {
        attrs: {
          label: "Send to All Devices",
        },
      }
    );
    checkSendToDeviceItems(expectedItems);

    // Done, hide the panel.
    let hiddenPromise = promisePageActionPanelHidden();
    BrowserPageActions.panelNode.hidePopup();
    await hiddenPromise;

    cleanUp();
  });
});

add_task(async function sendToDevice_title() {
  // Open two tabs that are sendable.
  await BrowserTestUtils.withNewTab("http://example.com/a", async otherBrowser => {
    await BrowserTestUtils.withNewTab("http://example.com/b", async () => {
      await promiseSyncReady();
      const sandbox = sinon.createSandbox();
      sandbox.stub(gSync, "syncReady").get(() => true);
      sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => true);
      sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
      sandbox.stub(gSync, "isSendableURI").returns(true);
      sandbox.stub(gSync, "sendTabTargets").get(() => []);
      sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type);

      let cleanUp = () => {
        sandbox.restore();
      };
      registerCleanupFunction(cleanUp);

      // Open the panel.  Only one tab is selected, so the action's title should
      // be "Send Tab to Device".
      await promisePageActionPanelOpen();
      let sendToDeviceButton =
        document.getElementById("pageAction-panel-sendToDevice");
      Assert.ok(!sendToDeviceButton.disabled);

      Assert.equal(sendToDeviceButton.label, "Send Tab to Device");
      Assert.equal(PageActions.actionForID("sendToDevice").getTitle(window),
                   "Send Tab to Device");

      // Hide the panel.
      let hiddenPromise = promisePageActionPanelHidden();
      BrowserPageActions.panelNode.hidePopup();
      await hiddenPromise;

      // Add the other tab to the selection.
      gBrowser.addToMultiSelectedTabs(gBrowser.getTabForBrowser(otherBrowser),
                                      false);

      // Open the panel again.  Now the action's title should be "Send 2 Tabs to
      // Device".
      await promisePageActionPanelOpen();
      Assert.ok(!sendToDeviceButton.disabled);
      Assert.equal(sendToDeviceButton.label, "Send 2 Tabs to Device");
      Assert.equal(PageActions.actionForID("sendToDevice").getTitle(window),
                   "Send 2 Tabs to Device");

      // Hide the panel.
      hiddenPromise = promisePageActionPanelHidden();
      BrowserPageActions.panelNode.hidePopup();
      await hiddenPromise;

      cleanUp();

      await UIState.reset();
    });
  });
});

add_task(async function sendToDevice_inUrlbar() {
  // Open a tab that's sendable.
  await BrowserTestUtils.withNewTab("http://example.com/", async () => {
    await promiseSyncReady();
    const sandbox = sinon.createSandbox();
    sandbox.stub(gSync, "syncReady").get(() => true);
    sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => true);
    sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
    sandbox.stub(gSync, "isSendableURI").returns(true);
    sandbox.stub(gSync, "sendTabTargets").get(() => mockTargets);
    sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type);

    let cleanUp = () => {
      sandbox.restore();
    };
    registerCleanupFunction(cleanUp);

    // Add Send to Device to the urlbar.
    let action = PageActions.actionForID("sendToDevice");
    action.pinnedToUrlbar = true;

    // Click it to open its panel.
    let urlbarButton = document.getElementById(
      BrowserPageActions.urlbarButtonNodeIDForActionID(action.id)
    );
    Assert.notEqual(urlbarButton, null, "The urlbar button should exist");
    Assert.ok(!urlbarButton.disabled,
              "The urlbar button should not be disabled");
    EventUtils.synthesizeMouseAtCenter(urlbarButton, {});
    // The panel element for _activatedActionPanelID is created synchronously
    // only after the associated button has been clicked.
    await promisePanelShown(BrowserPageActions._activatedActionPanelID);
    Assert.equal(urlbarButton.getAttribute("open"), "true",
      "Button has open attribute");

    // The devices should be shown in the subview.
    let expectedItems = [
      {
        className: "pageAction-sendToDevice-notReady",
        display: "none",
        disabled: true,
      },
    ];
    for (let target of mockTargets) {
      expectedItems.push({
        attrs: {
          clientId: target.id,
          label: target.name,
          clientType: target.type,
        },
      });
    }
    expectedItems.push(
      null,
      {
        attrs: {
          label: "Send to All Devices",
        },
      }
    );
    checkSendToDeviceItems(expectedItems, true);

    // Get the first device menu item in the panel.
    let bodyID =
      BrowserPageActions._panelViewNodeIDForActionID("sendToDevice", true) +
      "-body";
    let body = document.getElementById(bodyID);
    let deviceMenuItem = body.querySelector(".sendtab-target");
    Assert.notEqual(deviceMenuItem, null);

    // For good measure, wait until it's visible.
    let dwu = window.windowUtils;
    await BrowserTestUtils.waitForCondition(() => {
      let bounds = dwu.getBoundsWithoutFlushing(deviceMenuItem);
      return bounds.height > 0 && bounds.width > 0;
    }, "Waiting for first device menu item to appear");

    // Click it, which should cause the panel to close.
    let hiddenPromise =
      promisePanelHidden(BrowserPageActions._activatedActionPanelID);
    EventUtils.synthesizeMouseAtCenter(deviceMenuItem, {});
    info("Waiting for Send to Device panel to close after clicking a device");
    await hiddenPromise;
    Assert.ok(!urlbarButton.hasAttribute("open"),
      "URL bar button no longer has open attribute");

    // And then the "Sent!" notification panel should open and close by itself
    // after a moment.
    info("Waiting for the Sent! notification panel to open");
    await promisePanelShown(ConfirmationHint._panel.id);
    Assert.equal(
      ConfirmationHint._panel.anchorNode.id,
      urlbarButton.id
    );
    info("Waiting for the Sent! notification panel to close");
    await promisePanelHidden(ConfirmationHint._panel.id);

    // Remove Send to Device from the urlbar.
    action.pinnedToUrlbar = false;

    cleanUp();
  });
});

add_task(async function contextMenu() {
  // Open an actionable page so that the main page action button appears.
  let url = "http://example.com/";
  await BrowserTestUtils.withNewTab(url, async () => {
    // Open the panel and then open the context menu on the bookmark button.
    await promisePageActionPanelOpen();
    let bookmarkButton = document.getElementById("pageAction-panel-bookmark");
    let contextMenuPromise = promisePanelShown("pageActionContextMenu");
    EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
      type: "contextmenu",
      button: 2,
    });
    await contextMenuPromise;

    // The context menu should show the "remove" item.  Click it.
    let menuItems = collectContextMenuItems();
    Assert.equal(menuItems.length, 1,
                 "Context menu has one child");
    Assert.equal(menuItems[0].label, "Remove from Address Bar",
                 "Context menu is in the 'remove' state");
    contextMenuPromise = promisePanelHidden("pageActionContextMenu");
    EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
    await contextMenuPromise;

    // The action should be removed from the urlbar.  In this case, the bookmark
    // star, the node in the urlbar should be hidden.
    let starButtonBox = document.getElementById("star-button-box");
    await BrowserTestUtils.waitForCondition(() => {
      return starButtonBox.hidden;
    }, "Waiting for star button to become hidden");

    // Open the context menu again on the bookmark button.  (The page action
    // panel remains open.)
    contextMenuPromise = promisePanelShown("pageActionContextMenu");
    EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
      type: "contextmenu",
      button: 2,
    });
    await contextMenuPromise;

    // The context menu should show the "add" item.  Click it.
    menuItems = collectContextMenuItems();
    Assert.equal(menuItems.length, 1,
                 "Context menu has one child");
    Assert.equal(menuItems[0].label, "Add to Address Bar",
                 "Context menu is in the 'add' state");
    contextMenuPromise = promisePanelHidden("pageActionContextMenu");
    EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
    await contextMenuPromise;

    // The action should be added to the urlbar.
    await BrowserTestUtils.waitForCondition(() => {
      return !starButtonBox.hidden;
    }, "Waiting for star button to become unhidden");

    // Open the context menu on the bookmark star in the urlbar.
    contextMenuPromise = promisePanelShown("pageActionContextMenu");
    EventUtils.synthesizeMouseAtCenter(starButtonBox, {
      type: "contextmenu",
      button: 2,
    });
    await contextMenuPromise;

    // The context menu should show the "remove" item.  Click it.
    menuItems = collectContextMenuItems();
    Assert.equal(menuItems.length, 1,
                 "Context menu has one child");
    Assert.equal(menuItems[0].label, "Remove from Address Bar",
                 "Context menu is in the 'remove' state");
    contextMenuPromise = promisePanelHidden("pageActionContextMenu");
    EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
    await contextMenuPromise;

    // The action should be removed from the urlbar.
    await BrowserTestUtils.waitForCondition(() => {
      return starButtonBox.hidden;
    }, "Waiting for star button to become hidden");

    // Finally, add the bookmark star back to the urlbar so that other tests
    // that rely on it are OK.
    await promisePageActionPanelOpen();
    contextMenuPromise = promisePanelShown("pageActionContextMenu");
    EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
      type: "contextmenu",
      button: 2,
    });
    await contextMenuPromise;

    menuItems = collectContextMenuItems();
    Assert.equal(menuItems.length, 1,
                 "Context menu has one child");
    Assert.equal(menuItems[0].label, "Add to Address Bar",
                 "Context menu is in the 'add' state");
    contextMenuPromise = promisePanelHidden("pageActionContextMenu");
    EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
    await contextMenuPromise;
    await BrowserTestUtils.waitForCondition(() => {
      return !starButtonBox.hidden;
    }, "Waiting for star button to become unhidden");
  });

  // urlbar tests that run after this one can break if the mouse is left over
  // the area where the urlbar popup appears, which seems to happen due to the
  // above synthesized mouse events.  Move it over the urlbar.
  EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, { type: "mousemove" });
  gURLBar.focus();
});


function promiseSyncReady() {
  let service = Cc["@mozilla.org/weave/service;1"]
                  .getService(Ci.nsISupports)
                  .wrappedJSObject;
  return service.whenLoaded().then(() => {
    UIState.isReady();
    return UIState.refresh();
  });
}

function checkSendToDeviceItems(expectedItems, forUrlbar = false) {
  let bodyID =
    BrowserPageActions._panelViewNodeIDForActionID("sendToDevice", forUrlbar) +
    "-body";
  let body = document.getElementById(bodyID);
  Assert.equal(body.children.length, expectedItems.length);
  for (let i = 0; i < expectedItems.length; i++) {
    let expected = expectedItems[i];
    let actual = body.children[i];
    if (!expected) {
      Assert.equal(actual.localName, "toolbarseparator");
      continue;
    }
    if ("id" in expected) {
      Assert.equal(actual.id, expected.id);
    }
    if ("className" in expected) {
      let expectedNames = expected.className.split(/\s+/);
      for (let name of expectedNames) {
        Assert.ok(actual.classList.contains(name),
                  `classList contains: ${name}`);
      }
    }
    let display = "display" in expected ? expected.display : "-moz-box";
    Assert.equal(getComputedStyle(actual).display, display);
    let disabled = "disabled" in expected ? expected.disabled : false;
    Assert.equal(actual.disabled, disabled);
    if ("attrs" in expected) {
      for (let name in expected.attrs) {
        Assert.ok(actual.hasAttribute(name));
        let attrVal = actual.getAttribute(name);
        if (name == "label") {
          attrVal = attrVal.normalize("NFKC"); // There's a bug with …
        }
        Assert.equal(attrVal, expected.attrs[name]);
      }
    }
  }
}

function collectContextMenuItems() {
  let contextMenu = document.getElementById("pageActionContextMenu");
  return Array.prototype.filter.call(contextMenu.children, node => {
    return window.getComputedStyle(node).visibility == "visible";
  });
}
