// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Apple Inc. and contributors. // License: Licensed under MIT license (see license.js) // ==========================================================================

/*global module test equals context ok same Q$ htmlbody */

/*

We call RootResponder's browser event handlers directly with fake browser events. These
tests are to prove RootResponder's downstream behavior.

Our test pane has a view tree like so:

pane
 |--view1
 |  |--view1a
 |  |--view1b
 |--view2

Simulating responder-chain events on view1a allows us to ensure that view1 also receives
the event, but that view1b and view2 do not.

*/

// If we don't factory it, all the views share the same stub functions ergo the same callCounts. function viewClassFactory() {

return SC.View.extend({
  // Mouse
  mouseEntered: CoreTest.stub(),
  mouseMoved: CoreTest.stub(),
  mouseExited: CoreTest.stub(),
  // Click
  mouseDown: CoreTest.stub(),
  mouseUp: CoreTest.stub(),
  click: CoreTest.stub(),
  dblClick: CoreTest.stub(),
  // Touch
  touchDown: CoreTest.stub(),
  touchesMoved: CoreTest.stub(),
  touchUp: CoreTest.stub(),
  // Data
  dataDragEntered: CoreTest.stub(),
  dataDragHovered: CoreTest.stub(),
  dataDragExited: CoreTest.stub(),
  dataDragDropped: CoreTest.stub()
});

}

var pane, view1, view1a, view1b, view2, evt1a, evt2;

module(“Mouse event handling”, {

setup: function() {
  SC.run(function() {
    pane = SC.MainPane.create({
      childViews: ['view1', 'view2'],
      view1: viewClassFactory().extend({
        childViews: ['view1a', 'view1b'],
        view1a: viewClassFactory(),
        view1b: viewClassFactory()
      }),
      view2: viewClassFactory()
    });
    pane.append();
  });
  // Populate the variables for easy access.
  view1 = pane.get('view1');
  view1a = view1.get('view1a');
  view1b = view1.get('view1b');
  view2 = pane.get('view2');
  // Create the events.
  evt1a = {
    target: pane.getPath('view1.view1a.layer'),
    dataTransfer: { types: [] },
    preventDefault: CoreTest.stub(),
    stopPropagation: CoreTest.stub()
  };
  evt2 = {
    target: pane.getPath('view2.layer'),
    dataTransfer: { types: [] },
    preventDefault: CoreTest.stub(),
    stopPropagation: CoreTest.stub()
  };
},
teardown: function() {
  pane.remove();
  pane.destroy();
  pane = null;
  view1 = view1a = view1b = view2 = null;
  evt1a = evt2 = null;
}

});

test('Mouse movement', function() {

// Make sure we're all at zero.
// mouseEntered
var isGood = YES &&
  view1.mouseEntered.callCount === 0 &&
  view1a.mouseEntered.callCount === 0 &&
  view1b.mouseEntered.callCount === 0 &&
  view2.mouseEntered.callCount === 0;
ok(isGood, 'PRELIM: mouseEntered has not been called.');
// mouseMoved
isGood = YES &&
  view1.mouseMoved.callCount === 0 &&
  view1a.mouseMoved.callCount === 0 &&
  view1b.mouseMoved.callCount === 0 &&
  view2.mouseMoved.callCount === 0;
ok(isGood, 'PRELIM: mouseMoved has not been called.');
// mouseExited
isGood = YES &&
  view1.mouseExited.callCount === 0 &&
  view1a.mouseExited.callCount === 0 &&
  view1b.mouseExited.callCount === 0 &&
  view2.mouseExited.callCount === 0;
ok(isGood, 'PRELIM: mouseExited has not been called.');

// Move the mouse over view1a to trigger mouseEntered.
SC.RootResponder.responder.mousemove(evt1a);

equals(view1a.mouseEntered.callCount, 1, "The targeted view has received mouseEntered");
equals(view1.mouseEntered.callCount, 1, "The targeted view's parent has received mouseEntered");
equals(view1b.mouseEntered.callCount, 0, "The targeted view's sibling has NOT received mouseEntered");
equals(view2.mouseEntered.callCount, 0, "The targeted view's parent's sibling has NOT received mouseEntered");

isGood = YES &&
  view1.mouseMoved.callCount === 0 &&
  view1a.mouseMoved.callCount === 0 &&
  view1b.mouseMoved.callCount === 0 &&
  view2.mouseMoved.callCount === 0 &&
  view1.mouseExited.callCount === 0 &&
  view1a.mouseExited.callCount === 0 &&
  view1b.mouseExited.callCount === 0 &&
  view2.mouseExited.callCount === 0;
ok(isGood, 'No views have received mouseMoved or mouseExited.');

// Move the mouse over view1a again to trigger mouseMoved.
SC.RootResponder.responder.mousemove(evt1a);

equals(view1a.mouseMoved.callCount, 1, "The targeted view has received mouseMoved");
equals(view1.mouseMoved.callCount, 1, "The targeted view's parent has received mouseMoved");
equals(view1b.mouseMoved.callCount, 0, "The targeted view's sibling has NOT received mouseMoved");
equals(view2.mouseMoved.callCount, 0, "The targeted view's parent's sibling has NOT received mouseMoved");

isGood = YES &&
  view1.mouseEntered.callCount === 1 &&
  view1a.mouseEntered.callCount === 1 &&
  view1b.mouseEntered.callCount === 0 &&
  view2.mouseEntered.callCount === 0;
ok(isGood, "No views have received duplicate or out-of-place mouseEntered.");
isGood = YES &&
  view1.mouseExited.callCount === 0 &&
  view1a.mouseExited.callCount === 0 &&
  view1b.mouseExited.callCount === 0 &&
  view2.mouseExited.callCount === 0;
ok(isGood, 'No views have received mouseExited.');

// Move the mouse over view2 to trigger mouseExited on the initial responder chain.
SC.RootResponder.responder.mousemove(evt2);
equals(view1a.mouseExited.callCount, 1, "The targeted view has received mouseExited");
equals(view1.mouseExited.callCount, 1, "The targeted view's parent has received mouseExited");
equals(view1b.mouseExited.callCount, 0, "The targeted view's sibling has NOT received mouseExited");
equals(view2.mouseExited.callCount, 0, "The targeted view's parent's sibling (the new target) has NOT received mouseExited");
equals(view2.mouseEntered.callCount, 1, "The new target has received mouseEntered; circle of life");

});

test('mousemoved leaves a destroyed view without error', function() {

equals(view1a.mouseEntered.callCount, 0, "PRELIM: mouseEntered has not been called yet");
equals(view1a.mouseExited.callCount, 0, "PRELIM: mouseExited has not been called yet");

SC.run(function() {
  SC.RootResponder.responder.mousemove(evt1a);
});

equals(view1a.mouseEntered.callCount, 1, "The targeted view has received mouseEntered");

SC.run(function() {
  view1a.destroy();
});

SC.run(function() {
  SC.RootResponder.responder.mousemove(evt2);
});

equals(view1a.mouseExited.callCount, 0, "The destroyed view should not receive mouseExited");

});

/* TODO: Mouse clicks. */

/* TODO: Touch. */

test('Data dragging', function() {

// Make sure we're all at zero.
// dataDragEntered
var isGood = YES &&
  view1.dataDragEntered.callCount === 0 &&
  view1a.dataDragEntered.callCount === 0 &&
  view1b.dataDragEntered.callCount === 0 &&
  view2.dataDragEntered.callCount === 0;
ok(isGood, 'PRELIM: dataDragEntered has not been called.');
// dataDragHovered
isGood = YES &&
  view1.dataDragHovered.callCount === 0 &&
  view1a.dataDragHovered.callCount === 0 &&
  view1b.dataDragHovered.callCount === 0 &&
  view2.dataDragHovered.callCount === 0;
ok(isGood, 'PRELIM: dataDragHovered has not been called.');
// dataDragExited
isGood = YES &&
  view1.dataDragExited.callCount === 0 &&
  view1a.dataDragExited.callCount === 0 &&
  view1b.dataDragExited.callCount === 0 &&
  view2.dataDragExited.callCount === 0;
ok(isGood, 'PRELIM: dataDragExited has not been called.');

// Drag the mouse over view1a to trigger mouseEntered.
evt1a.type = 'dragenter';
SC.RootResponder.responder.dragenter(evt1a);

// Test the views.
equals(view1a.dataDragEntered.callCount, 1, "The targeted view has received dataDragEntered");
equals(view1a.dataDragHovered.callCount, 1, "The targeted view has received initial dataDragHovered");
equals(view1.dataDragEntered.callCount, 1, "The targeted view's parent has received dataDragEntered");
equals(view1.dataDragHovered.callCount, 1, "The targeted view's parent has received initial dataDragHovered");
equals(view1b.dataDragEntered.callCount + view1b.dataDragHovered.callCount, 0, "The targeted view's sibling has NOT received dataDragEntered or dataDragHovered");
equals(view2.dataDragEntered.callCount + view2.dataDragHovered.callCount, 0, "The targeted view's parent's sibling has NOT received dataDragEntered or dataDragHovered");

isGood = YES &&
  view1.dataDragExited.callCount === 0 &&
  view1a.dataDragExited.callCount === 0 &&
  view1b.dataDragExited.callCount === 0 &&
  view2.dataDragExited.callCount === 0;
ok(isGood, 'No views have received dataDragExited.');

// Hover the drag and make sure only dataDragHovered is called.
evt1a.type = 'dragover';
SC.RootResponder.responder.dragover(evt1a);

// Test the views.
equals(view1a.dataDragHovered.callCount, 2, "The targeted view has received another dataDragHovered");
equals(view1.dataDragHovered.callCount, 2, "The targeted view's parent has received another dataDragHovered");
equals(view1b.dataDragHovered.callCount, 0, "The targeted view's sibling has NOT received dataDragHovered");
equals(view2.dataDragHovered.callCount, 0, "The targeted view's parent's sibling has NOT received dataDragHovered");
equals(view1b.dataDragEntered.callCount + view1b.dataDragHovered.callCount, 0, "The targeted view's sibling has NOT received dataDragEntered or dataDragHovered");
equals(view2.dataDragEntered.callCount + view2.dataDragHovered.callCount, 0, "The targeted view's parent's sibling has NOT received dataDragEntered or dataDragHovered");

isGood = YES &&
  view1.dataDragEntered.callCount === 1 &&
  view1a.dataDragEntered.callCount === 1 &&
  view1b.dataDragEntered.callCount === 0 &&
  view2.dataDragEntered.callCount === 0;
ok(isGood, "No views have received duplicate or out-of-place dataDragEntered.");
isGood = YES &&
  view1.dataDragExited.callCount === 0 &&
  view1a.dataDragExited.callCount === 0 &&
  view1b.dataDragExited.callCount === 0 &&
  view2.dataDragExited.callCount === 0;
ok(isGood, 'No views have received dataDragExited.');

// Leave view1a and enter view2 to trigger dataDragExited on the initial responder chain.
// Note that browsers call the new dragenter prior to the old dragleave.
evt2.type = 'dragenter';
evt1a.type = 'dragleave';
SC.RootResponder.responder.dragenter(evt2);
SC.RootResponder.responder.dragleave(evt1a);

// Check the views.
equals(view1a.dataDragExited.callCount, 1, "The targeted view has received dataDragExited");
equals(view1.dataDragExited.callCount, 1, "The targeted view's parent has received dataDragExited");
equals(view1b.dataDragExited.callCount, 0, "The targeted view's sibling has NOT received dataDragExited");
equals(view2.dataDragExited.callCount, 0, "The targeted view's parent's sibling (the new target) has NOT received dataDragExited");
equals(view2.dataDragEntered.callCount, 1, "The new target has received dataDragEntered; circle of life");

// Leave view2 to test document leaving.
evt2.type = 'dragleave';
SC.RootResponder.responder.dragleave(evt2);

// Check the views.
equals(view1a.dataDragExited.callCount, 1, "The previously-targeted view has NOT received additional dataDragExited on document exit");
equals(view1.dataDragExited.callCount, 1, "The previously-targeted view's parent has received dataDragExited");
equals(view1b.dataDragExited.callCount, 0, "The previously-targeted view's sibling has NOT received dataDragExited ever basically");
equals(view2.dataDragEntered.callCount, 1, "The new target has NOT received additional dataDragEntered on document exit");
equals(view2.dataDragExited.callCount, 1, "The new target has received dataDragExited on document exit");

// TODO: Test the 300ms timer to make sure the force-drag-leave works for Firefox (et al. probably).

// Test drop.
evt1a.type = 'dragenter';
SC.RootResponder.responder.dragenter(evt1a);
evt1a.type = 'dragdrop';
SC.RootResponder.responder.drop(evt1a);

// Check the views.
equals(view1a.dataDragDropped.callCount, 1, "The targeted view received a dataDragDropped event");
equals(view1.dataDragDropped.callCount, 0, "The targeted view's parent did not receive a dataDragDropped event");

});

test('Data dragging content types', function() {

// Drag the event over view 1a with type 'Files' (should cancel).
evt1a.dataTransfer.types = ['Files'];
evt1a.dataTransfer.dropEffect = 'copy';
SC.RootResponder.responder.dragover(evt1a);

equals(evt1a.preventDefault.callCount, 1, "The default behavior was prevented for a 'Files' drag");
equals(evt1a.dataTransfer.dropEffect, 'none', "The drop effect was set to 'none' for a 'Files' drag");

// Drag the event over view 1a with type 'text/uri-list' (should cancel).
evt1a.dataTransfer.types = ['text/uri-list'];
evt1a.dataTransfer.dropEffect = 'copy';
SC.RootResponder.responder.dragover(evt1a);

equals(evt1a.preventDefault.callCount, 2, "The default behavior was prevented for a 'text/uri-list' drag");
equals(evt1a.dataTransfer.dropEffect, 'none', "The drop effect was set to 'none' for a 'text/uri-list' drag");

// Drag the event over view 1a with type 'text/plain' (should not cancel).
evt1a.dataTransfer.types = ['text/plain'];
evt1a.dataTransfer.dropEffect = 'copy';
SC.RootResponder.responder.dragover(evt1a);

equals(evt1a.preventDefault.callCount, 2, "The default behavior was NOT prevented for a 'text/plain' drag");
equals(evt1a.dataTransfer.dropEffect, 'copy', "The drop effect was NOT changed for a 'text/plain' drag");

});