/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';

module.metadata = {
  'engines': {
    'Firefox': '> 28'
  }
};

const { Cu } = require('chrome');
const { Loader } = require('sdk/test/loader');
const { data } = require('sdk/self');
const { open, focus, close } = require('sdk/window/helpers');
const { setTimeout } = require('sdk/timers');
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
const { partial } = require('sdk/lang/functional');
const { wait } = require('./event/helpers');
const { gc } = require('sdk/test/memory');
const packaging = require("@loader/options");

const openBrowserWindow = partial(open, null, {features: {toolbar: true}});
const openPrivateBrowserWindow = partial(open, null,
  {features: {toolbar: true, private: true}});

const badgeNodeFor = (node) =>
  node.ownerDocument.getAnonymousElementByAttribute(node,
                                      'class', 'toolbarbutton-badge');

function getWidget(buttonId, window = getMostRecentBrowserWindow()) {
  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
  const { AREA_NAVBAR } = CustomizableUI;

  let widgets = CustomizableUI.getWidgetIdsInArea(AREA_NAVBAR).
    filter((id) => id.startsWith('toggle-button--') && id.endsWith(buttonId));

  if (widgets.length === 0)
    throw new Error('Widget with id `' + id +'` not found.');

  if (widgets.length > 1)
    throw new Error('Unexpected number of widgets: ' + widgets.length)

  return CustomizableUI.getWidget(widgets[0]).forWindow(window);
};

exports['test basic constructor validation'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  assert.throws(
    () => ToggleButton({}),
    /^The option/,
    'throws on no option given');

  // Test no label
  assert.throws(
    () => ToggleButton({ id: 'my-button', icon: './icon.png'}),
    /^The option "label"/,
    'throws on no label given');

  // Test no id
  assert.throws(
    () => ToggleButton({ label: 'my button', icon: './icon.png' }),
    /^The option "id"/,
    'throws on no id given');

  // Test no icon
  assert.throws(
    () => ToggleButton({ id: 'my-button', label: 'my button' }),
    /^The option "icon"/,
    'throws on no icon given');


  // Test empty label
  assert.throws(
    () => ToggleButton({ id: 'my-button', label: '', icon: './icon.png' }),
    /^The option "label"/,
    'throws on no valid label given');

  // Test invalid id
  assert.throws(
    () => ToggleButton({ id: 'my button', label: 'my button', icon: './icon.png' }),
    /^The option "id"/,
    'throws on no valid id given');

  // Test empty id
  assert.throws(
    () => ToggleButton({ id: '', label: 'my button', icon: './icon.png' }),
    /^The option "id"/,
    'throws on no valid id given');

  // Test remote icon
  assert.throws(
    () => ToggleButton({ id: 'my-button', label: 'my button', icon: 'http://www.mozilla.org/favicon.ico'}),
    /^The option "icon"/,
    'throws on no valid icon given');

  // Test wrong icon: no absolute URI to local resource, neither relative './'
  assert.throws(
    () => ToggleButton({ id: 'my-button', label: 'my button', icon: 'icon.png'}),
    /^The option "icon"/,
    'throws on no valid icon given');

  // Test wrong icon: no absolute URI to local resource, neither relative './'
  assert.throws(
    () => ToggleButton({ id: 'my-button', label: 'my button', icon: 'foo and bar'}),
    /^The option "icon"/,
    'throws on no valid icon given');

  // Test wrong icon: '../' is not allowed
  assert.throws(
    () => ToggleButton({ id: 'my-button', label: 'my button', icon: '../icon.png'}),
    /^The option "icon"/,
    'throws on no valid icon given');

  assert.throws(
    () => ToggleButton({ id: 'my-button', label: 'button', icon: './i.png', badge: true}),
    /^The option "badge"/,
    'throws on no valid badge given');

  assert.throws(
    () => ToggleButton({ id: 'my-button', label: 'button', icon: './i.png', badgeColor: true}),
    /^The option "badgeColor"/,
    'throws on no valid badge given');

  loader.unload();
};

exports['test button added'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let button = ToggleButton({
    id: 'my-button-1',
    label: 'my button',
    icon: './icon.png'
  });

  // check defaults
  assert.equal(button.disabled, false,
    'disabled is set to default `false` value');

  let { node } = getWidget(button.id);

  assert.ok(!!node, 'The button is in the navbar');

  assert.equal(button.label, node.getAttribute('label'),
    'label is set');

  assert.equal(button.label, node.getAttribute('tooltiptext'),
    'tooltip is set');

  assert.equal(data.url(button.icon.substr(2)), node.getAttribute('image'),
    'icon is set');

  assert.equal("", node.getAttribute('badge'),
    'badge attribute is empty');

  loader.unload();
}
exports['test button is not garbaged'] = function (assert, done) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  ToggleButton({
    id: 'my-button-1',
    label: 'my button',
    icon: './icon.png',
    onClick: () => {
      loader.unload();
      done();
    }
  });

  gc().then(() => {
    let { node } = getWidget('my-button-1');

    assert.ok(!!node, 'The button is in the navbar');

    assert.equal('my button', node.getAttribute('label'),
      'label is set');

    assert.equal(data.url('icon.png'), node.getAttribute('image'),
      'icon is set');

    // ensure the listener is not gc'ed too
    node.click();
  }).catch(assert.fail);
}

exports['test button added with resource URI'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let button = ToggleButton({
    id: 'my-button-1',
    label: 'my button',
    icon: data.url('icon.png')
  });

  assert.equal(button.icon, data.url('icon.png'),
    'icon is set');

  let { node } = getWidget(button.id);

  assert.equal(button.icon, node.getAttribute('image'),
    'icon on node is set');

  loader.unload();
}

exports['test button duplicate id'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let button = ToggleButton({
    id: 'my-button-2',
    label: 'my button',
    icon: './icon.png'
  });

  assert.throws(() => {
    let doppelganger = ToggleButton({
      id: 'my-button-2',
      label: 'my button',
      icon: './icon.png'
    });
  },
  /^The ID/,
  'No duplicates allowed');

  loader.unload();
}

exports['test button multiple destroy'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let button = ToggleButton({
    id: 'my-button-2',
    label: 'my button',
    icon: './icon.png'
  });

  button.destroy();
  button.destroy();
  button.destroy();

  assert.pass('multiple destroy doesn\'t matter');

  loader.unload();
}

exports['test button removed on dispose'] = function(assert, done) {
  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let widgetId;

  CustomizableUI.addListener({
    onWidgetDestroyed: function(id) {
      if (id === widgetId) {
        CustomizableUI.removeListener(this);

        assert.pass('button properly removed');
        loader.unload();
        done();
      }
    }
  });

  let button = ToggleButton({
    id: 'my-button-3',
    label: 'my button',
    icon: './icon.png'
  });

  // Tried to use `getWidgetIdsInArea` but seems undefined, not sure if it
  // was removed or it's not in the UX build yet
  widgetId = getWidget(button.id).id;

  button.destroy();
};

exports['test button global state updated'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let button = ToggleButton({
    id: 'my-button-4',
    label: 'my button',
    icon: './icon.png',
  });

  // Tried to use `getWidgetIdsInArea` but seems undefined, not sure if it
  // was removed or it's not in the UX build yet

  let { node, id: widgetId } = getWidget(button.id);

  // check read-only properties

  assert.throws(() => button.id = 'another-id',
    /^setting getter-only property/,
    'id cannot be set at runtime');

  assert.equal(button.id, 'my-button-4',
    'id is unchanged');
  assert.equal(node.id, widgetId,
    'node id is unchanged');

  // check writable properties

  button.label = 'New label';
  assert.equal(button.label, 'New label',
    'label is updated');
  assert.equal(node.getAttribute('label'), 'New label',
    'node label is updated');
  assert.equal(node.getAttribute('tooltiptext'), 'New label',
    'node tooltip is updated');

  button.icon = './new-icon.png';
  assert.equal(button.icon, './new-icon.png',
    'icon is updated');
  assert.equal(node.getAttribute('image'), data.url('new-icon.png'),
    'node image is updated');

  button.disabled = true;
  assert.equal(button.disabled, true,
    'disabled is updated');
  assert.equal(node.getAttribute('disabled'), 'true',
    'node disabled is updated');

  button.badge = '+2';
  button.badgeColor = 'blue';

  assert.equal(button.badge, '+2',
    'badge is updated');
  assert.equal(node.getAttribute('bagde'), '',
    'node badge is updated');

  assert.equal(button.badgeColor, 'blue',
    'badgeColor is updated');
  assert.equal(badgeNodeFor(node).style.backgroundColor, 'blue',
    'badge color is updated');

  // TODO: test validation on update

  loader.unload();
}

exports['test button global state set and get with state method'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let button = ToggleButton({
    id: 'my-button-16',
    label: 'my button',
    icon: './icon.png'
  });

  // read the button's state
  let state = button.state(button);

  assert.equal(state.label, 'my button',
    'label is correct');
  assert.equal(state.icon, './icon.png',
    'icon is correct');
  assert.equal(state.disabled, false,
    'disabled is correct');

  // set the new button's state
  button.state(button, {
    label: 'New label',
    icon: './new-icon.png',
    disabled: true,
    badge: '+2',
    badgeColor: 'blue'
  });

  assert.equal(button.label, 'New label',
    'label is updated');
  assert.equal(button.icon, './new-icon.png',
    'icon is updated');
  assert.equal(button.disabled, true,
    'disabled is updated');
  assert.equal(button.badge, '+2',
    'badge is updated');
  assert.equal(button.badgeColor, 'blue',
    'badgeColor is updated');

  loader.unload();
}

exports['test button global state updated on multiple windows'] = function*(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let button = ToggleButton({
    id: 'my-button-5',
    label: 'my button',
    icon: './icon.png'
  });

  let nodes = [getWidget(button.id).node];

  let window = yield openBrowserWindow();

  nodes.push(getWidget(button.id, window).node);

  button.label = 'New label';
  button.icon = './new-icon.png';
  button.disabled = true;
  button.badge = '+10';
  button.badgeColor = 'green';

  for (let node of nodes) {
    assert.equal(node.getAttribute('label'), 'New label',
      'node label is updated');
    assert.equal(node.getAttribute('tooltiptext'), 'New label',
      'node tooltip is updated');

    assert.equal(button.icon, './new-icon.png',
      'icon is updated');
    assert.equal(node.getAttribute('image'), data.url('new-icon.png'),
      'node image is updated');

    assert.equal(button.disabled, true,
      'disabled is updated');
    assert.equal(node.getAttribute('disabled'), 'true',
      'node disabled is updated');

    assert.equal(button.badge, '+10',
      'badge is updated')
    assert.equal(button.badgeColor, 'green',
      'badgeColor is updated')
    assert.equal(node.getAttribute('badge'), '+10',
      'node badge is updated')
    assert.equal(badgeNodeFor(node).style.backgroundColor, 'green',
      'node badge color is updated')
  };

  yield close(window);

  loader.unload();
};

exports['test button window state'] = function*(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let { browserWindows } = loader.require('sdk/windows');

  let button = ToggleButton({
    id: 'my-button-6',
    label: 'my button',
    icon: './icon.png',
    badge: '+1',
    badgeColor: 'red'
  });

  let mainWindow = browserWindows.activeWindow;
  let nodes = [getWidget(button.id).node];

  let window = yield openBrowserWindow().then(focus);

  nodes.push(getWidget(button.id, window).node);

  let { activeWindow } = browserWindows;

  button.state(activeWindow, {
    label: 'New label',
    icon: './new-icon.png',
    disabled: true,
    badge: '+2',
    badgeColor : 'green'
  });

  // check the states

  assert.equal(button.label, 'my button',
    'global label unchanged');
  assert.equal(button.icon, './icon.png',
    'global icon unchanged');
  assert.equal(button.disabled, false,
    'global disabled unchanged');
  assert.equal(button.badge, '+1',
    'global badge unchanged');
  assert.equal(button.badgeColor, 'red',
    'global badgeColor unchanged');

  let state = button.state(mainWindow);

  assert.equal(state.label, 'my button',
    'previous window label unchanged');
  assert.equal(state.icon, './icon.png',
    'previous window icon unchanged');
  assert.equal(state.disabled, false,
    'previous window disabled unchanged');
  assert.deepEqual(button.badge, '+1',
    'previouswindow badge unchanged');
  assert.deepEqual(button.badgeColor, 'red',
    'previous window badgeColor unchanged');

  state = button.state(activeWindow);

  assert.equal(state.label, 'New label',
    'active window label updated');
  assert.equal(state.icon, './new-icon.png',
    'active window icon updated');
  assert.equal(state.disabled, true,
    'active disabled updated');
  assert.equal(state.badge, '+2',
    'active badge updated');
  assert.equal(state.badgeColor, 'green',
    'active badgeColor updated');

  // change the global state, only the windows without a state are affected

  button.label = 'A good label';
  button.badge = '+3';

  assert.equal(button.label, 'A good label',
    'global label updated');
  assert.equal(button.state(mainWindow).label, 'A good label',
    'previous window label updated');
  assert.equal(button.state(activeWindow).label, 'New label',
    'active window label unchanged');
  assert.equal(button.state(activeWindow).badge, '+2',
    'active badge unchanged');
  assert.equal(button.state(activeWindow).badgeColor, 'green',
    'active badgeColor unchanged');
  assert.equal(button.state(mainWindow).badge, '+3',
    'previous window badge updated');
  assert.equal(button.state(mainWindow).badgeColor, 'red',
    'previous window badgeColor unchanged');

  // delete the window state will inherits the global state again

  button.state(activeWindow, null);

  state = button.state(activeWindow);

  assert.equal(state.label, 'A good label',
    'active window label inherited');
  assert.equal(state.badge, '+3',
    'previous window badge inherited');
  assert.equal(button.badgeColor, 'red',
    'previous window badgeColor inherited');

  // check the nodes properties
  let node = nodes[0];

  state = button.state(mainWindow);

  assert.equal(node.getAttribute('label'), state.label,
    'node label is correct');
  assert.equal(node.getAttribute('tooltiptext'), state.label,
    'node tooltip is correct');

  assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
    'node image is correct');
  assert.equal(node.hasAttribute('disabled'), state.disabled,
    'disabled is correct');
  assert.equal(node.getAttribute("badge"), state.badge,
    'badge is correct');

  assert.equal(badgeNodeFor(node).style.backgroundColor, state.badgeColor,
    'badge color is correct');

  node = nodes[1];
  state = button.state(activeWindow);

  assert.equal(node.getAttribute('label'), state.label,
    'node label is correct');
  assert.equal(node.getAttribute('tooltiptext'), state.label,
    'node tooltip is correct');

  assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
    'node image is correct');
  assert.equal(node.hasAttribute('disabled'), state.disabled,
    'disabled is correct');
  assert.equal(node.getAttribute('badge'), state.badge,
    'badge is correct');

  assert.equal(badgeNodeFor(node).style.backgroundColor, state.badgeColor,
    'badge color is correct');

  yield close(window);

  loader.unload();
};


exports['test button tab state'] = function*(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let { browserWindows } = loader.require('sdk/windows');
  let tabs = loader.require('sdk/tabs');

  let button = ToggleButton({
    id: 'my-button-7',
    label: 'my button',
    icon: './icon.png'
  });

  let mainTab = tabs.activeTab;
  let node = getWidget(button.id).node;

  tabs.open('about:blank');

  yield wait(tabs, 'ready');

  let tab = tabs.activeTab;
  let { activeWindow } = browserWindows;

  // set window state
  button.state(activeWindow, {
    label: 'Window label',
    icon: './window-icon.png',
    badge: 'win',
    badgeColor: 'blue'
  });

  // set previous active tab state
  button.state(mainTab, {
    label: 'Tab label',
    icon: './tab-icon.png',
    badge: 'tab',
    badgeColor: 'red'
  });

  // set current active tab state
  button.state(tab, {
    icon: './another-tab-icon.png',
    disabled: true,
    badge: 't1',
    badgeColor: 'green'
  });

  // check the states, be sure they won't be gc'ed
  yield gc();

  assert.equal(button.label, 'my button',
    'global label unchanged');
  assert.equal(button.icon, './icon.png',
    'global icon unchanged');
  assert.equal(button.disabled, false,
    'global disabled unchanged');
  assert.equal(button.badge, undefined,
    'global badge unchanged')

  let state = button.state(mainTab);

  assert.equal(state.label, 'Tab label',
    'previous tab label updated');
  assert.equal(state.icon, './tab-icon.png',
    'previous tab icon updated');
  assert.equal(state.disabled, false,
    'previous tab disabled unchanged');
  assert.equal(state.badge, 'tab',
    'previous tab badge unchanged')
  assert.equal(state.badgeColor, 'red',
    'previous tab badgeColor unchanged')

  state = button.state(tab);

  assert.equal(state.label, 'Window label',
    'active tab inherited from window state');
  assert.equal(state.icon, './another-tab-icon.png',
    'active tab icon updated');
  assert.equal(state.disabled, true,
    'active disabled updated');
  assert.equal(state.badge, 't1',
    'active badge updated');
  assert.equal(state.badgeColor, 'green',
    'active badgeColor updated');

  // change the global state
  button.icon = './good-icon.png';

  // delete the tab state
  button.state(tab, null);

  assert.equal(button.icon, './good-icon.png',
    'global icon updated');
  assert.equal(button.state(mainTab).icon, './tab-icon.png',
    'previous tab icon unchanged');
  assert.equal(button.state(tab).icon, './window-icon.png',
    'tab icon inherited from window');
  assert.equal(button.state(mainTab).badge, 'tab',
    'previous tab badge is unchaged');
  assert.equal(button.state(tab).badge, 'win',
    'tab badge is inherited from window');

  // delete the window state
  button.state(activeWindow, null);

  state = button.state(tab);

  assert.equal(state.icon, './good-icon.png',
    'tab icon inherited from global');
  assert.equal(state.badge, undefined,
    'tab badge inherited from global');
  assert.equal(state.badgeColor, undefined,
    'tab badgeColor inherited from global');

  // check the node properties
  yield wait();

  assert.equal(node.getAttribute('label'), state.label,
    'node label is correct');
  assert.equal(node.getAttribute('tooltiptext'), state.label,
    'node tooltip is correct');
  assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
    'node image is correct');
  assert.equal(node.hasAttribute('disabled'), state.disabled,
    'node disabled is correct');
  assert.equal(node.getAttribute('badge'), '',
    'badge text is correct');
  assert.equal(badgeNodeFor(node).style.backgroundColor, '',
    'badge color is correct');

  mainTab.activate();

  yield wait(tabs, 'activate');

  // This is made in order to avoid to check the node before it
  // is updated, need a better check
  yield wait();

  state = button.state(mainTab);

  assert.equal(node.getAttribute('label'), state.label,
    'node label is correct');
  assert.equal(node.getAttribute('tooltiptext'), state.label,
    'node tooltip is correct');
  assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
    'node image is correct');
  assert.equal(node.hasAttribute('disabled'), state.disabled,
    'disabled is correct');
  assert.equal(node.getAttribute('badge'), state.badge,
    'badge text is correct');
  assert.equal(badgeNodeFor(node).style.backgroundColor, state.badgeColor,
    'badge color is correct');

  tab.close(loader.unload);

  loader.unload();
};

exports['test button click'] = function*(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let { browserWindows } = loader.require('sdk/windows');

  let labels = [];

  let button = ToggleButton({
    id: 'my-button-8',
    label: 'my button',
    icon: './icon.png',
    onClick: ({label}) => labels.push(label)
  });

  let mainWindow = browserWindows.activeWindow;
  let chromeWindow = getMostRecentBrowserWindow();

  let window = yield openBrowserWindow().then(focus);

  button.state(mainWindow, { label: 'nothing' });
  button.state(mainWindow.tabs.activeTab, { label: 'foo'})
  button.state(browserWindows.activeWindow, { label: 'bar' });

  button.click();

  yield focus(chromeWindow);

  button.click();

  assert.deepEqual(labels, ['bar', 'foo'],
    'button click works');

  yield close(window);

  loader.unload();
}

exports['test button icon set'] = function(assert) {
  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  // Test remote icon set
  assert.throws(
    () => ToggleButton({
      id: 'my-button-10',
      label: 'my button',
      icon: {
        '16': 'http://www.mozilla.org/favicon.ico'
      }
    }),
    /^The option "icon"/,
    'throws on no valid icon given');

  let button = ToggleButton({
    id: 'my-button-11',
    label: 'my button',
    icon: {
      '5': './icon5.png',
      '16': './icon16.png',
      '32': './icon32.png',
      '64': './icon64.png'
    }
  });

  let { node, id: widgetId } = getWidget(button.id);
  let { devicePixelRatio } = node.ownerGlobal;

  let size = 16 * devicePixelRatio;

  assert.equal(node.getAttribute('image'), data.url(button.icon[size].substr(2)),
    'the icon is set properly in navbar');

  size = 32 * devicePixelRatio;

  CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_PANEL);

  assert.equal(node.getAttribute('image'), data.url(button.icon[size].substr(2)),
    'the icon is set properly in panel');

  // Using `loader.unload` without move back the button to the original area
  // raises an error in the CustomizableUI. This is doesn't happen if the
  // button is moved manually from navbar to panel. I believe it has to do
  // with `addWidgetToArea` method, because even with a `timeout` the issue
  // persist.
  CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR);

  loader.unload();
}

exports['test button icon set with only one option'] = function(assert) {
  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  // Test remote icon set
  assert.throws(
    () => ToggleButton({
      id: 'my-button-10',
      label: 'my button',
      icon: {
        '16': 'http://www.mozilla.org/favicon.ico'
      }
    }),
    /^The option "icon"/,
    'throws on no valid icon given');

  let button = ToggleButton({
    id: 'my-button-11',
    label: 'my button',
    icon: {
      '5': './icon5.png'
    }
  });

  let { node, id: widgetId } = getWidget(button.id);

  assert.equal(node.getAttribute('image'), data.url(button.icon['5'].substr(2)),
    'the icon is set properly in navbar');

  CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_PANEL);

  assert.equal(node.getAttribute('image'), data.url(button.icon['5'].substr(2)),
    'the icon is set properly in panel');

  // Using `loader.unload` without move back the button to the original area
  // raises an error in the CustomizableUI. This is doesn't happen if the
  // button is moved manually from navbar to panel. I believe it has to do
  // with `addWidgetToArea` method, because even with a `timeout` the issue
  // persist.
  CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR);

  loader.unload();
}

exports['test button state validation'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let { browserWindows } = loader.require('sdk/windows');

  let button = ToggleButton({
    id: 'my-button-12',
    label: 'my button',
    icon: './icon.png'
  })

  let state = button.state(button);

  assert.throws(
    () => button.state(button, { icon: 'http://www.mozilla.org/favicon.ico' }),
    /^The option "icon"/,
    'throws on remote icon given');

  assert.throws(
    () => button.state(button, { badge: true } ),
    /^The option "badge"/,
    'throws on wrong badge value given');

  loader.unload();
};

exports['test button are not in private windows'] = function(assert, done) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let{ isPrivate } = loader.require('sdk/private-browsing');
  let { browserWindows } = loader.require('sdk/windows');

  let button = ToggleButton({
    id: 'my-button-13',
    label: 'my button',
    icon: './icon.png'
  });

  openPrivateBrowserWindow().then(window => {
    assert.ok(isPrivate(window),
      'the new window is private');

    let { node } = getWidget(button.id, window);

    assert.ok(!node || node.style.display === 'none',
      'the button is not added / is not visible on private window');

    return window;
  }).
  then(close).
  then(loader.unload).
  then(done, assert.fail)
}

exports['test button state are snapshot'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let { browserWindows } = loader.require('sdk/windows');
  let tabs = loader.require('sdk/tabs');

  let button = ToggleButton({
    id: 'my-button-14',
    label: 'my button',
    icon: './icon.png'
  });

  let state = button.state(button);
  let windowState = button.state(browserWindows.activeWindow);
  let tabState = button.state(tabs.activeTab);

  assert.deepEqual(windowState, state,
    'window state has the same properties of button state');

  assert.deepEqual(tabState, state,
    'tab state has the same properties of button state');

  assert.notEqual(windowState, state,
    'window state is not the same object of button state');

  assert.notEqual(tabState, state,
    'tab state is not the same object of button state');

  assert.deepEqual(button.state(button), state,
    'button state has the same content of previous button state');

  assert.deepEqual(button.state(browserWindows.activeWindow), windowState,
    'window state has the same content of previous window state');

  assert.deepEqual(button.state(tabs.activeTab), tabState,
    'tab state has the same content of previous tab state');

  assert.notEqual(button.state(button), state,
    'button state is not the same object of previous button state');

  assert.notEqual(button.state(browserWindows.activeWindow), windowState,
    'window state is not the same object of previous window state');

  assert.notEqual(button.state(tabs.activeTab), tabState,
    'tab state is not the same object of previous tab state');

  loader.unload();
}

exports['test button icon object is a snapshot'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let icon = {
    '16': './foo.png'
  };

  let button = ToggleButton({
    id: 'my-button-17',
    label: 'my button',
    icon: icon
  });

  assert.deepEqual(button.icon, icon,
    'button.icon has the same properties of the object set in the constructor');

  assert.notEqual(button.icon, icon,
    'button.icon is not the same object of the object set in the constructor');

  assert.throws(
    () => button.icon[16] = './bar.png',
    /16 is read-only/,
    'properties of button.icon are ready-only'
  );

  let newIcon = {'16': './bar.png'};
  button.icon = newIcon;

  assert.deepEqual(button.icon, newIcon,
    'button.icon has the same properties of the object set');

  assert.notEqual(button.icon, newIcon,
    'button.icon is not the same object of the object set');

  loader.unload();
}

exports['test button after destroy'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let { browserWindows } = loader.require('sdk/windows');
  let { activeTab } = loader.require('sdk/tabs');

  let button = ToggleButton({
    id: 'my-button-15',
    label: 'my button',
    icon: './icon.png',
    onClick: () => assert.fail('onClick should not be called')
  });

  button.destroy();

  assert.throws(
    () => button.click(),
    /^The state cannot be set or get/,
    'button.click() not executed');

  assert.throws(
    () => button.label,
    /^The state cannot be set or get/,
    'button.label cannot be get after destroy');

  assert.throws(
    () => button.label = 'my label',
    /^The state cannot be set or get/,
    'button.label cannot be set after destroy');

  assert.throws(
    () => {
      button.state(browserWindows.activeWindow, {
        label: 'window label'
      });
    },
    /^The state cannot be set or get/,
    'window state label cannot be set after destroy');

  assert.throws(
    () => button.state(browserWindows.activeWindow).label,
    /^The state cannot be set or get/,
    'window state label cannot be get after destroy');

  assert.throws(
    () => {
      button.state(activeTab, {
        label: 'tab label'
      });
    },
    /^The state cannot be set or get/,
    'tab state label cannot be set after destroy');

  assert.throws(
    () => button.state(activeTab).label,
    /^The state cannot be set or get/,
    'window state label cannot se get after destroy');

  loader.unload();
};

exports['test button badge property'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let button = ToggleButton({
    id: 'my-button-18',
    label: 'my button',
    icon: './icon.png',
    badge: 123456
  });

  assert.equal(button.badge, 123456,
    'badge is set');

  assert.equal(button.badgeColor, undefined,
    'badge color is not set');

  let { node } = getWidget(button.id);
  let { getComputedStyle } = node.ownerGlobal;
  let badgeNode = badgeNodeFor(node);

  assert.equal('1234', node.getAttribute('badge'),
    'badge text is displayed up to four characters');

  assert.equal(getComputedStyle(badgeNode).backgroundColor, 'rgb(217, 0, 0)',
    'badge color is the default one');

  button.badge = '危機';

  assert.equal(button.badge, '危機',
    'badge is properly set');

  assert.equal('危機', node.getAttribute('badge'),
    'badge text is displayed');

  button.badge = '🐶🐰🐹';

  assert.equal(button.badge, '🐶🐰🐹',
    'badge is properly set');

  assert.equal('🐶🐰🐹', node.getAttribute('badge'),
    'badge text is displayed');

  loader.unload();
}
exports['test button badge color'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');

  let button = ToggleButton({
    id: 'my-button-19',
    label: 'my button',
    icon: './icon.png',
    badge: '+1',
    badgeColor: 'blue'
  });

  assert.equal(button.badgeColor, 'blue',
    'badge color is set');

  let { node } = getWidget(button.id);
  let { getComputedStyle } = node.ownerGlobal;
  let badgeNode = badgeNodeFor(node);

  assert.equal(badgeNodeFor(node).style.backgroundColor, 'blue',
    'badge color is displayed properly');
  assert.equal(getComputedStyle(badgeNode).backgroundColor, 'rgb(0, 0, 255)',
    'badge color overrides the default one');

  loader.unload();
}

// toggle button only
exports['test button checked'] = function(assert, done) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let { browserWindows } = loader.require('sdk/windows');

  let events = [];

  let button = ToggleButton({
    id: 'my-button-9',
    label: 'my button',
    icon: './icon.png',
    checked: true,
    onClick: ({label}) => events.push('clicked:' + label),
    onChange: state => events.push('changed:' + state.label + ':' + state.checked)
  });

  let { node } = getWidget(button.id);

  assert.equal(node.getAttribute('type'), 'checkbox',
    'node type is properly set');

  let mainWindow = browserWindows.activeWindow;
  let chromeWindow = getMostRecentBrowserWindow();

  openBrowserWindow().then(focus).then(window => {
    button.state(mainWindow, { label: 'nothing' });
    button.state(mainWindow.tabs.activeTab, { label: 'foo'})
    button.state(browserWindows.activeWindow, { label: 'bar' });

    button.click();
    button.click();

    focus(chromeWindow).then(() => {
      button.click();
      button.click();

      assert.deepEqual(events, [
          'clicked:bar', 'changed:bar:false', 'clicked:bar', 'changed:bar:true',
          'clicked:foo', 'changed:foo:false', 'clicked:foo', 'changed:foo:true'
        ],
        'button change events works');

      close(window).
        then(loader.unload).
        then(done, assert.fail);
    })
  }).then(null, assert.fail);
}

exports['test button is checked on window level'] = function(assert, done) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let { browserWindows } = loader.require('sdk/windows');
  let tabs = loader.require('sdk/tabs');

  let button = ToggleButton({
    id: 'my-button-20',
    label: 'my button',
    icon: './icon.png'
  });

  let mainWindow = browserWindows.activeWindow;
  let mainTab = tabs.activeTab;

  assert.equal(button.checked, false,
    'global state, checked is `false`.');
  assert.equal(button.state(mainTab).checked, false,
    'tab state, checked is `false`.');
  assert.equal(button.state(mainWindow).checked, false,
    'window state, checked is `false`.');

  button.click();

  tabs.open({
    url: 'about:blank',
    onActivate: function onActivate(tab) {
      tab.removeListener('activate', onActivate);

      assert.notEqual(mainTab, tab,
        'the current tab is not the same.');

      assert.equal(button.checked, false,
        'global state, checked is `false`.');
      assert.equal(button.state(mainTab).checked, true,
        'previous tab state, checked is `true`.');
      assert.equal(button.state(tab).checked, true,
        'current tab state, checked is `true`.');
      assert.equal(button.state(mainWindow).checked, true,
        'window state, checked is `true`.');

      openBrowserWindow().then(focus).then(window => {
        let { activeWindow } = browserWindows;
        let { activeTab } = activeWindow.tabs;

        assert.equal(button.checked, false,
          'global state, checked is `false`.');
        assert.equal(button.state(activeTab).checked, false,
          'tab state, checked is `false`.');

        assert.equal(button.state(activeWindow).checked, false,
          'window state, checked is `false`.');

        tab.close(()=> {
          close(window).
            then(loader.unload).
            then(done, assert.fail);
        })
      }).
      then(null, assert.fail);
    }
  });

};

exports['test button click do not messing up states'] = function(assert) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let { browserWindows } = loader.require('sdk/windows');

  let button = ToggleButton({
    id: 'my-button-21',
    label: 'my button',
    icon: './icon.png'
  });

  let mainWindow = browserWindows.activeWindow;
  let { activeTab } = mainWindow.tabs;

  button.state(mainWindow, { icon: './new-icon.png' });
  button.state(activeTab, { label: 'foo'})

  assert.equal(button.state(mainWindow).label, 'my button',
    'label property for window state, properly derived from global state');

  assert.equal(button.state(activeTab).icon, './new-icon.png',
    'icon property for tab state, properly derived from window state');

  button.click();

  button.label = 'bar';

  assert.equal(button.state(mainWindow).label, 'bar',
    'label property for window state, properly derived from global state');

  button.state(mainWindow, null);

  assert.equal(button.state(activeTab).icon, './icon.png',
    'icon property for tab state, properly derived from window state');

  loader.unload();
}

exports['test buttons can have anchored panels'] = function(assert, done) {
  let loader = Loader(module);
  let { ToggleButton } = loader.require('sdk/ui');
  let { Panel } = loader.require('sdk/panel');
  let { identify } = loader.require('sdk/ui/id');
  let { getActiveView } = loader.require('sdk/view/core');

  let b1 = ToggleButton({
    id: 'my-button-22',
    label: 'my button',
    icon: './icon.png',
    onChange: ({checked}) => checked && panel.show()
  });

  let b2 = ToggleButton({
    id: 'my-button-23',
    label: 'my button',
    icon: './icon.png',
    onChange: ({checked}) => checked && panel.show({position: b2})
  });

  let panel = Panel({
    position: b1
  });

  let { document } = getMostRecentBrowserWindow();
  let b1Node = document.getElementById(identify(b1));
  let b2Node = document.getElementById(identify(b2));
  let panelNode = getActiveView(panel);

  panel.once('show', () => {
    assert.ok(b1.state('window').checked,
      'button is checked');

    assert.equal(panelNode.getAttribute('type'), 'arrow',
      'the panel is a arrow type');

    assert.strictEqual(b1Node, panelNode.anchorNode,
      'the panel is anchored properly to the button given in costructor');

    panel.hide();

    panel.once('show', () => {
      assert.ok(b2.state('window').checked,
        'button is checked');

      assert.equal(panelNode.getAttribute('type'), 'arrow',
        'the panel is a arrow type');

      // test also that the button passed in `show` method, takes the precedence
      // over the button set in panel's constructor.
      assert.strictEqual(b2Node, panelNode.anchorNode,
        'the panel is anchored properly to the button passed to show method');

      loader.unload();

      done();
    });

    b2.click();
  });

  b1.click();
}


if (packaging.isNative) {
  module.exports = {
    "test skip on jpm": (assert) => assert.pass("skipping this file with jpm")
  };
}

require("sdk/test").run(module.exports);
