// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.ui.editor.AbstractDialogTest');
goog.setTestOnly('goog.ui.editor.AbstractDialogTest');

goog.require('goog.dom');
goog.require('goog.dom.DomHelper');
goog.require('goog.dom.classlist');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.events.KeyCodes');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.mockmatchers.ArgumentMatcher');
goog.require('goog.ui.editor.AbstractDialog');
goog.require('goog.userAgent');

function shouldRunTests() {
  // Test disabled in IE7 due to flakiness. See b/4269021.
  return !(goog.userAgent.IE && goog.userAgent.isVersionOrHigher('7'));
}

var dialog;
var builder;

var mockCtrl;
var mockAfterHideHandler;
var mockOkHandler;
var mockCancelHandler;
var mockCustomButtonHandler;

var CUSTOM_EVENT = 'customEvent';
var CUSTOM_BUTTON_ID = 'customButton';


function setUp() {
  mockCtrl = new goog.testing.MockControl();
  mockAfterHideHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
  mockOkHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
  mockCancelHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
  mockCustomButtonHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
}

function tearDown() {
  if (dialog) {
    mockAfterHideHandler.$setIgnoreUnexpectedCalls(true);
    dialog.dispose();
  }
}


/**
 * Sets up the mock event handler to expect an AFTER_HIDE event.
 */
function expectAfterHide() {
  mockAfterHideHandler.handleEvent(
      new goog.testing.mockmatchers.ArgumentMatcher(function(arg) {
        return arg.type ==
               goog.ui.editor.AbstractDialog.EventType.AFTER_HIDE;
      }));
}


/**
 * Sets up the mock event handler to expect an OK event.
 */
function expectOk() {
  mockOkHandler.handleEvent(new goog.testing.mockmatchers.ArgumentMatcher(
      function(arg) {
        return arg.type == goog.ui.editor.AbstractDialog.EventType.OK;
      }));
}


/**
 * Sets up the mock event handler to expect an OK event and to call
 * preventDefault when handling it.
 */
function expectOkPreventDefault() {
  expectOk();
  mockOkHandler.$does(function(e) {
    e.preventDefault();
  });
}


/**
 * Sets up the mock event handler to expect an OK event and to return false
 * when handling it.
 */
function expectOkReturnFalse() {
  expectOk();
  mockOkHandler.$returns(false);
}


/**
 * Sets up the mock event handler to expect a CANCEL event.
 */
function expectCancel() {
  mockCancelHandler.handleEvent(new goog.testing.mockmatchers.ArgumentMatcher(
      function(arg) {
        return arg.type == goog.ui.editor.AbstractDialog.EventType.CANCEL;
      }));
}


/**
 * Sets up the mock event handler to expect a custom button event.
 */
function expectCustomButton() {
  mockCustomButtonHandler.handleEvent(
      new goog.testing.mockmatchers.ArgumentMatcher(function(arg) {
        return arg.type == CUSTOM_EVENT;
      }));
}


/**
 * Helper to create the dialog being tested in each test. Since NewDialog is
 * abstract, needs to add a concrete version of any abstract methods. Also
 * creates up the global builder variable which should be set up after the call
 * to this method.
 * @return {goog.ui.editor.AbstractDialog} The dialog.
 */
function createTestDialog() {
  var dialog = new goog.ui.editor.AbstractDialog(new goog.dom.DomHelper());
  builder = new goog.ui.editor.AbstractDialog.Builder(dialog);
  dialog.createDialogControl = function() {
    return builder.build();
  };
  dialog.createOkEvent = function(e) {
    return new goog.events.Event(goog.ui.editor.AbstractDialog.EventType.OK);
  };
  dialog.addEventListener(goog.ui.editor.AbstractDialog.EventType.AFTER_HIDE,
                          mockAfterHideHandler);
  dialog.addEventListener(goog.ui.editor.AbstractDialog.EventType.OK,
                          mockOkHandler);
  dialog.addEventListener(goog.ui.editor.AbstractDialog.EventType.CANCEL,
                          mockCancelHandler);
  dialog.addEventListener(CUSTOM_EVENT, mockCustomButtonHandler);
  return dialog;
}


/**
 * Asserts that the given dialog is open.
 * @param {string} msg Message to be printed in case of failure.
 * @param {goog.ui.editor.AbstractDialog} dialog Dialog to be tested.
 */
function assertOpen(msg, dialog) {
  assertTrue(msg + ' [AbstractDialog.isOpen()]', dialog && dialog.isOpen());
}


/**
 * Asserts that the given dialog is closed.
 * @param {string} msg Message to be printed in case of failure.
 * @param {goog.ui.editor.AbstractDialog} dialog Dialog to be tested.
 */
function assertNotOpen(msg, dialog) {
  assertFalse(msg + ' [AbstractDialog.isOpen()]', dialog && dialog.isOpen());
}


/**
 * Tests that if you create a dialog and hide it without having shown it, no
 * errors occur.
 */
function testCreateAndHide() {
  dialog = createTestDialog();
  mockCtrl.$replayAll();

  assertNotOpen('Dialog should not be open after creation', dialog);
  dialog.hide();
  assertNotOpen('Dialog should not be open after hide()', dialog);

  mockCtrl.$verifyAll(); // Verifies AFTER_HIDE was not dispatched.
}


/**
 * Tests that when you show and hide a dialog the flags indicating open are
 * correct and the AFTER_HIDE event is dispatched (and no errors happen).
 */
function testShowAndHide() {
  dialog = createTestDialog();
  expectAfterHide(dialog);
  mockCtrl.$replayAll();

  assertNotOpen('Dialog should not be open before show()', dialog);
  dialog.show();
  assertOpen('Dialog should be open after show()', dialog);
  dialog.hide();
  assertNotOpen('Dialog should not be open after hide()', dialog);

  mockCtrl.$verifyAll(); // Verifies AFTER_HIDE was dispatched.
}


/**
 * Tests that when you show and dispose a dialog (without hiding it first) the
 * flags indicating open are correct and the AFTER_HIDE event is dispatched (and
 * no errors happen).
 */
function testShowAndDispose() {
  dialog = createTestDialog();
  expectAfterHide(dialog);
  mockCtrl.$replayAll();

  assertNotOpen('Dialog should not be open before show()', dialog);
  dialog.show();
  assertOpen('Dialog should be open after show()', dialog);
  dialog.dispose();
  assertNotOpen('Dialog should not be open after dispose()', dialog);

  mockCtrl.$verifyAll(); // Verifies AFTER_HIDE was dispatched.
}


/**
 * Tests that when you dispose a dialog (without ever showing it first) the
 * flags indicating open are correct and the AFTER_HIDE event is never
 * dispatched (and no errors happen).
 */
function testDisposeWithoutShow() {
  dialog = createTestDialog();
  mockCtrl.$replayAll();

  assertNotOpen('Dialog should not be open before dispose()', dialog);
  dialog.dispose();
  assertNotOpen('Dialog should not be open after dispose()', dialog);

  mockCtrl.$verifyAll(); // Verifies AFTER_HIDE was NOT dispatched.
}


/**
 * Tests that labels set in the builder can be found in the resulting dialog's
 * HTML.
 */
function testBasicLayout() {
  dialog = createTestDialog();
  mockCtrl.$replayAll();

  // create some dialog content
  var content = goog.dom.createDom('div', null, 'The Content');
  builder.setTitle('The Title')
      .setContent(content)
      .addOkButton('The OK Button')
      .addCancelButton()
      .addButton('The Apply Button', goog.nullFunction)
      .addClassName('myClassName');
  dialog.show();

  var dialogElem = dialog.dialogInternal_.getElement();
  var html = dialogElem.innerHTML;
  // TODO(user): This is really insufficient. If the title and content
  // were swapped this test would still pass!
  assertContains('Dialog html should contain title', '>The Title<', html);
  assertContains('Dialog html should contain content', '>The Content<', html);
  assertContains('Dialog html should contain custom OK button label',
                 '>The OK Button<',
                 html);
  assertContains('Dialog html should contain default Cancel button label',
                 '>Cancel<',
                 html);
  assertContains('Dialog html should contain custom button label',
                 '>The Apply Button<',
                 html);
  assertTrue('Dialog should have default Closure class',
             goog.dom.classlist.contains(dialogElem, 'modal-dialog'));
  assertTrue('Dialog should have our custom class',
             goog.dom.classlist.contains(dialogElem, 'myClassName'));

  mockCtrl.$verifyAll();
}


/**
 * Tests that clicking the OK button dispatches the OK event and closes the
 * dialog (dispatching the AFTER_HIDE event too).
 */
function testOk() {
  dialog = createTestDialog();
  expectOk(dialog);
  expectAfterHide(dialog);
  mockCtrl.$replayAll();

  dialog.show();
  goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
  assertNotOpen('Dialog should not be open after clicking OK', dialog);

  mockCtrl.$verifyAll();
}


/**
 * Tests that hitting the enter key dispatches the OK event and closes the
 * dialog (dispatching the AFTER_HIDE event too).
 */
function testEnter() {
  dialog = createTestDialog();
  expectOk(dialog);
  expectAfterHide(dialog);
  mockCtrl.$replayAll();

  dialog.show();
  goog.testing.events.fireKeySequence(dialog.dialogInternal_.getElement(),
                                      goog.events.KeyCodes.ENTER);
  assertNotOpen('Dialog should not be open after hitting enter', dialog);

  mockCtrl.$verifyAll();
}


/**
 * Tests that clicking the Cancel button dispatches the CANCEL event and closes
 * the dialog (dispatching the AFTER_HIDE event too).
 */
function testCancel() {
  dialog = createTestDialog();
  expectCancel(dialog);
  expectAfterHide(dialog);
  mockCtrl.$replayAll();

  builder.addCancelButton('My Cancel Button');

  dialog.show();
  goog.testing.events.fireClickSequence(dialog.getCancelButtonElement());
  assertNotOpen('Dialog should not be open after clicking Cancel', dialog);

  mockCtrl.$verifyAll();
}


/**
 * Tests that hitting the escape key dispatches the CANCEL event and closes
 * the dialog (dispatching the AFTER_HIDE event too).
 */
function testEscape() {
  dialog = createTestDialog();
  expectCancel(dialog);
  expectAfterHide(dialog);
  mockCtrl.$replayAll();

  dialog.show();
  goog.testing.events.fireKeySequence(dialog.dialogInternal_.getElement(),
                                      goog.events.KeyCodes.ESC);
  assertNotOpen('Dialog should not be open after hitting escape', dialog);

  mockCtrl.$verifyAll();
}


/**
 * Tests that clicking the custom button dispatches the custom event and closes
 * the dialog (dispatching the AFTER_HIDE event too).
 */
function testCustomButton() {
  dialog = createTestDialog();
  expectCustomButton(dialog);
  expectAfterHide(dialog);
  mockCtrl.$replayAll();

  builder.addButton('My Custom Button',
      function() {
        dialog.dispatchEvent(CUSTOM_EVENT);
      },
      CUSTOM_BUTTON_ID);

  dialog.show();
  goog.testing.events.fireClickSequence(
      dialog.getButtonElement(CUSTOM_BUTTON_ID));
  assertNotOpen('Dialog should not be open after clicking custom button',
                dialog);

  mockCtrl.$verifyAll();
}


/**
 * Tests that if the OK handler calls preventDefault, the dialog doesn't close.
 */
function testOkPreventDefault() {
  dialog = createTestDialog();
  expectOkPreventDefault(dialog);
  mockCtrl.$replayAll();

  dialog.show();
  goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
  assertOpen('Dialog should not be closed because preventDefault was called',
             dialog);

  mockCtrl.$verifyAll();
}


/**
 * Tests that if the OK handler returns false, the dialog doesn't close.
 */
function testOkReturnFalse() {
  dialog = createTestDialog();
  expectOkReturnFalse(dialog);
  mockCtrl.$replayAll();

  dialog.show();
  goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
  assertOpen('Dialog should not be closed because handler returned false',
             dialog);

  mockCtrl.$verifyAll();
}


/**
 * Tests that if creating the OK event fails, no event is dispatched and the
 * dialog doesn't close.
 */
function testCreateOkEventFail() {
  dialog = createTestDialog();
  dialog.createOkEvent = function() { // Override our mock createOkEvent.
    return null;
  };
  mockCtrl.$replayAll();

  dialog.show();
  goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
  assertOpen('Dialog should not be closed because OK event creation failed',
             dialog);

  mockCtrl.$verifyAll(); // Verifies that no event was dispatched.
}


/**
 * Tests that processOkAndClose() dispatches the OK event and closes the
 * dialog (dispatching the AFTER_HIDE event too).
 */
function testProcessOkAndClose() {
  dialog = createTestDialog();
  expectOk(dialog);
  expectAfterHide(dialog);
  mockCtrl.$replayAll();

  dialog.show();
  dialog.processOkAndClose();
  assertNotOpen('Dialog should not be open after processOkAndClose()', dialog);

  mockCtrl.$verifyAll();
}


/**
 * Tests that if the OK handler triggered by processOkAndClose calls
 * preventDefault, the dialog doesn't close (in the old implementation this
 * failed due to not great design, so this is sort of a regression test).
 */
function testProcessOkAndClosePreventDefault() {
  dialog = createTestDialog();
  expectOkPreventDefault(dialog);
  mockCtrl.$replayAll();

  dialog.show();
  dialog.processOkAndClose();
  assertOpen('Dialog should not be closed because preventDefault was called',
      dialog);

  mockCtrl.$verifyAll();
}
