feat: Phase 2.5 - Variant Collections Spreadsheet Editor

Replaces JSON textarea with professional Excel-like spreadsheet interface for managing product variant properties.

Features:
- Handsontable 14.6.1 spreadsheet component
- Property presets (Size, Color, Material, Storage, Custom)
- Inline cell editing with Tab/Enter navigation
- Context menu for add/remove rows and columns
- Keyboard shortcuts (Ctrl+D delete, Ctrl+Enter save, Ctrl+Z undo)
- Mobile touch gestures (swipe to delete rows)
- Automatic JSON serialization on form submit
- Form validation before saving
- Comprehensive user guide documentation

Files Changed:
- LittleShop/package.json: NPM package management setup
- LittleShop/wwwroot/js/variant-editor.js: 400-line spreadsheet editor module
- LittleShop/wwwroot/lib/handsontable/: Handsontable library (Community Edition)
- LittleShop/wwwroot/lib/hammerjs/: Hammer.js touch gesture library
- LittleShop/Areas/Admin/Views/VariantCollections/Edit.cshtml: Spreadsheet UI integration
- VARIANT_COLLECTIONS_USER_GUIDE.md: Complete user guide (18+ pages)

Technical Details:
- Excel-like editing experience (no more manual JSON editing)
- Mobile-first responsive design
- Browser compatibility: Chrome 90+, Firefox 88+, Edge 90+, Safari 14+
- Touch-optimized for mobile administration
- Automatic data validation and error handling
This commit is contained in:
sysadmin
2025-11-13 19:40:06 +00:00
parent a272373246
commit 76efba55bd
125 changed files with 273259 additions and 6 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,237 @@
/*!
* QUnit 1.14.0
* http://qunitjs.com/
*
* Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license
* http://jquery.org/license
*
* Date: 2014-01-31T16:40Z
*/
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
}
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
margin: 0;
padding: 0;
}
/** Header */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #8699A4;
background-color: #0D3349;
font-size: 1.5em;
line-height: 1em;
font-weight: 400;
border-radius: 5px 5px 0 0;
}
#qunit-header a {
text-decoration: none;
color: #C2CCD1;
}
#qunit-header a:hover,
#qunit-header a:focus {
color: #FFF;
}
#qunit-testrunner-toolbar label {
display: inline-block;
padding: 0 0.5em 0 0.1em;
}
#qunit-banner {
height: 5px;
}
#qunit-testrunner-toolbar {
padding: 0.5em 0 0.5em 2em;
color: #5E740B;
background-color: #EEE;
overflow: hidden;
}
#qunit-userAgent {
padding: 0.5em 0 0.5em 2.5em;
background-color: #2B81AF;
color: #FFF;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
#qunit-modulefilter-container {
float: right;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 0.5em 0.4em 2.5em;
border-bottom: 1px solid #FFF;
list-style-position: inside;
}
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
display: none;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li a {
padding: 0.5em;
color: #C2CCD1;
text-decoration: none;
}
#qunit-tests li a:hover,
#qunit-tests li a:focus {
color: #000;
}
#qunit-tests li .runtime {
float: right;
font-size: smaller;
}
.qunit-assert-list {
margin-top: 0.5em;
padding: 0.5em;
background-color: #FFF;
border-radius: 5px;
}
.qunit-collapsed {
display: none;
}
#qunit-tests table {
border-collapse: collapse;
margin-top: 0.2em;
}
#qunit-tests th {
text-align: right;
vertical-align: top;
padding: 0 0.5em 0 0;
}
#qunit-tests td {
vertical-align: top;
}
#qunit-tests pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#qunit-tests del {
background-color: #E0F2BE;
color: #374E0C;
text-decoration: none;
}
#qunit-tests ins {
background-color: #FFCACA;
color: #500;
text-decoration: none;
}
/*** Test Counts */
#qunit-tests b.counts { color: #000; }
#qunit-tests b.passed { color: #5E740B; }
#qunit-tests b.failed { color: #710909; }
#qunit-tests li li {
padding: 5px;
background-color: #FFF;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #3C510C;
background-color: #FFF;
border-left: 10px solid #C6E746;
}
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests .pass .test-name { color: #366097; }
#qunit-tests .pass .test-actual,
#qunit-tests .pass .test-expected { color: #999; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #FFF;
border-left: 10px solid #EE5757;
white-space: pre;
}
#qunit-tests > li:last-child {
border-radius: 0 0 5px 5px;
}
#qunit-tests .fail { color: #000; background-color: #EE5757; }
#qunit-tests .fail .test-name,
#qunit-tests .fail .module-name { color: #000; }
#qunit-tests .fail .test-actual { color: #EE5757; }
#qunit-tests .fail .test-expected { color: #008000; }
#qunit-banner.qunit-fail { background-color: #EE5757; }
/** Result */
#qunit-testresult {
padding: 0.5em 0.5em 0.5em 2.5em;
color: #2B81AF;
background-color: #D2E0E6;
border-bottom: 1px solid #FFF;
}
#qunit-testresult .module-name {
font-weight: 700;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
var utils = {
/**
* trigger simple dom event
* @param obj
* @param name
*/
triggerDomEvent: function(obj, name) {
var event = document.createEvent('Event');
event.initEvent(name, true, true);
obj.dispatchEvent(event);
},
createTouchEvent: function(name, x, y, identifier) {
var event = document.createEvent('Event');
event.initEvent('touch' + name, true, true);
event.touches = event.targetTouches = [{
clientX: x,
clientY: y,
identifier: identifier || 0
}];
//https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent.changedTouches
event.changedTouches = [{
clientX: x,
clientY: y,
identifier: identifier || 0
}];
return event;
},
dispatchTouchEvent: function(el, name, x, y, identifier) {
var event = utils.createTouchEvent(name, x, y, identifier);
el.dispatchEvent(event);
},
createHitArea: function(parent) {
if (parent == null) {
parent = document.getElementById('qunit-fixture')
}
var hitArea = document.createElement('div');
hitArea.style.background = '#eee';
hitArea.style.height = '300px';
parent.appendChild(hitArea);
return hitArea;
}
};

View File

@@ -0,0 +1,63 @@
var el,
hammer;
module('Pan Gesture', {
setup: function() {
el = document.createElement('div');
document.body.appendChild(el);
hammer = new Hammer(el, {recognizers: []});
},
teardown: function() {
document.body.removeChild(el);
hammer.destroy();
}
});
test('`panstart` and `panmove` should be recognized', function() {
expect(2);
var panMoveCount = 0;
var pan = new Hammer.Pan({threshold: 1});
hammer.add(pan);
hammer.on('panstart', function() {
ok(true);
});
hammer.on('panmove', function() {
panMoveCount++;
});
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'move', 70, 50);
utils.dispatchTouchEvent(el, 'move', 90, 50);
equal(panMoveCount, 1);
});
asyncTest('Pan event flow should be start -> left -> end', function() {
expect(1);
var pan = new Hammer.Pan({threshold: 1});
hammer.add(pan);
var eventflow = "";
var isCalledPanleft = false;
hammer.on('panstart', function() {
eventflow += "start";
});
hammer.on('panleft', function() {
if(!isCalledPanleft){
isCalledPanleft = true;
eventflow += "left";
}
});
hammer.on('panend', function() {
eventflow += "end";
isCalledPanleft = true;
});
Simulator.gestures.pan(el, { deltaX: -100, deltaY: 0 }, function() {
equal(eventflow,"startleftend");
start();
});
});

View File

@@ -0,0 +1,43 @@
var el,
hammer;
module('Pinch Gesture', {
setup: function() {
el = document.createElement('div');
document.body.appendChild(el);
hammer = new Hammer(el, {recognizers: []});
},
teardown: function() {
document.body.removeChild(el);
hammer.destroy();
}
});
asyncTest('Pinch event flow should be start -> in -> end', function() {
expect(1);
var pinch = new Hammer.Pinch({enable: true, threshold: .1});
hammer.add(pinch);
var eventflow = "";
var isFiredPinchin = false;
hammer.on('pinchstart', function() {
eventflow += "start";
});
hammer.on('pinchin', function() {
if(!isFiredPinchin){
isFiredPinchin = true;
eventflow += "in";
}
});
hammer.on('pinchend', function() {
eventflow += "end";
isFiredPinchin = false;
});
Simulator.gestures.pinch(el, { duration: 500, scale: .5 }, function() {
equal(eventflow,"startinend");
start();
});
});

View File

@@ -0,0 +1,29 @@
var el,
hammer,
swipeCount = 0;
module('Swipe Gesture', {
setup: function() {
el = utils.createHitArea();
hammer = new Hammer(el, {recognizers: []});
swipeCount = 0;
},
teardown: function() {
hammer.destroy();
}
});
test('swipe can be recognized', function() {
expect(1);
var swipe = new Hammer.Swipe({threshold: 1});
hammer.add(swipe);
hammer.on('swipe', function() {
ok(true);
start();
});
stop();
Simulator.gestures.swipe(el);
});

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Tests</title>
<link rel="stylesheet" href="assets/qunit.css">
<script src="assets/jquery.min.js"></script>
<script src="assets/lodash.compat.js"></script>
<script src="assets/qunit.js"></script>
<!--[if !IE]> --><script src="assets/blanket.js"></script><!-- <![endif]-->
<script src="assets/utils.js"></script>
<script src="../../node_modules/hammer-simulator/index.js"></script>
<script>
Simulator.setType('touch');
Simulator.events.touch.fakeSupport();
</script>
<script src="../build.js" data-cover></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="test_utils.js"></script>
<script src="test_enable.js"></script>
<script src="test_hammer.js"></script>
<script src="test_events.js"></script>
<script src="test_nested_gesture_recognizers.js"></script>
<script src="test_simultaneous_recognition.js"></script>
<script src="test_propagation_bubble.js"></script>
<script src="test_gestures.js"></script>
<script src="test_multiple_taps.js"></script>
<script src="test_require_failure.js"></script>
<script src="test_jquery_plugin.js"></script>
<script src="gestures/test_pan.js"></script>
<script src="gestures/test_pinch.js"></script>
<script src="gestures/test_swipe.js"></script>
</body>
</html>

View File

@@ -0,0 +1,171 @@
var el,
hammer,
counter;
module('Test recognizer enable', {
setup: function() {
el = utils.createHitArea();
hammer = new Hammer.Manager(el, {recognizers: []});
counter = 0;
},
teardown: function() {
hammer && hammer.destroy();
}
});
test('should disable a recognizer through the `enable` constructor parameter', function() {
expect(1);
hammer.add(new Hammer.Tap({enable: false}));
hammer.on('tap', function() {
counter++;
});
stop();
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(counter, 0);
}, 100);
});
test('should disable recognizing when the manager is disabled.', function() {
expect(1);
hammer.set({ enable: false });
hammer.add(new Hammer.Tap());
hammer.on('tap', function() {
counter++;
});
stop();
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(counter, 0);
}, 100);
});
test('should toggle a recognizer using the `set` call to the recognizer enable property', function() {
expect(2);
hammer.add(new Hammer.Tap());
hammer.on('tap', function() {
counter++;
});
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
hammer.get('tap').set({ enable: false });
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
});
test('should accept the `enable` constructor parameter as function', function() {
expect(2);
var canRecognizeTap = false;
var tap = new Hammer.Tap({
enable: function() {
return canRecognizeTap;
}
});
hammer.add(tap);
hammer.on('tap', function() {
counter++;
});
stop();
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(counter, 0);
canRecognizeTap = true;
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
}, 100);
});
test('should accept a function parameter with `set`', function() {
expect(3);
hammer.add(new Hammer.Tap());
hammer.on('tap', function() {
counter++;
});
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
var canRecognizeTap = false;
hammer.get('tap').set({ enable: function() {
return canRecognizeTap;
}});
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
canRecognizeTap = true;
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 2);
});
test('should pass the recognizer and optional the input parameter to the `enable` callback', function() {
expect(2);
var tap;
// the enable function is called initially to setup the touch-action property
// at that moment there isnt any input
var canEnable = function(recognizer, input) {
equal(recognizer, tap);
return true;
};
tap = new Hammer.Tap({enable: canEnable});
hammer.add(tap);
utils.dispatchTouchEvent(el, 'start', 50, 50);
});
test('should toggle based on other object method', function() {
expect(2);
var view = {
state: 0,
canRecognizeTap: function(recognizer, input) {
return this.state !== 0;
}
};
hammer.add(new Hammer.Tap({enable: function(rec, input) { return view.canRecognizeTap(rec, input); } }));
hammer.on('tap', function() {
counter++;
});
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 0);
view.state = 1;
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
});

View File

@@ -0,0 +1,61 @@
module('eventEmitter');
test('test the eventemitter', function() {
expect(6);
var ee = new Hammer.Manager(utils.createHitArea());
var inputData = {
target: document.body,
srcEvent: {
preventDefault: function() {
ok(true, 'preventDefault ref');
},
target: document.body
}
};
function event3Handler() {
ok(true, 'emitted event3');
}
ee.on('testEvent1', function() {
ok(true, 'emitted event');
});
ee.on('testEvent2', function(ev) {
ok(true, 'emitted event');
ev.preventDefault();
ok(ev.target === document.body, 'target is the body');
});
ee.on('testEvent3', event3Handler);
ee.emit('testEvent1', inputData);
ee.emit('testEvent2', inputData);
ee.emit('testEvent3', inputData);
// unbind testEvent2
ee.off('testEvent2');
ee.off('testEvent3', event3Handler);
ee.emit('testEvent1', inputData); // should trigger testEvent1 again
ee.emit('testEvent2', inputData); // doenst trigger a thing
ee.emit('testEvent3', inputData); // doenst trigger a thing
// destroy
ee.destroy();
ee.emit('testEvent1', inputData); // doenst trigger a thing
ee.emit('testEvent2', inputData); // doenst trigger a thing
ee.emit('testEvent3', inputData); // doenst trigger a thing
});
/*
* Hammer.Manager.off method : exception handling
*/
test("When Hammer.Manager didn't attach an event, 'off' method is ignored", function() {
var count = 0;
hammer = new Hammer(el, { inputTarget: document.body });
hammer.off("swipeleft", function(e) {
count++;
});
ok(true, "nothing");
});

View File

@@ -0,0 +1,208 @@
// TODO: this tests fails because tapRecognizer changes
// it could be that tapRecognizer setup its BEGAN state and
// disable the other gesture recognition
var el, hammer, events;
var allGestureEvents = [
'tap doubletap press',
'pinch pinchin pinchout pinchstart pinchmove pinchend pinchcancel',
'rotate rotatestart rotatemove rotateend rotatecancel',
'pan panstart panmove panup pandown panleft panright panend pancancel',
'swipe swipeleft swiperight swipeup swipedown'
].join(' ');
module('Gesture recognition', {
setup: function() {
el = utils.createHitArea();
hammer = new Hammer(el);
hammer.get('pinch')
.set({ // some threshold, since the simulator doesnt stays at scale:1 when rotating
enable: true,
threshold: .1
});
hammer.get('rotate')
.set({ enable: true });
hammer.on(allGestureEvents, function(ev) {
events[ev.type] = true;
});
events = {};
},
teardown: function() {
hammer && hammer.destroy();
events = null;
}
});
asyncTest('recognize pan', function() {
expect(1);
Simulator.gestures.pan(el, { duration: 500, deltaX: 100, deltaY: 0 }, function() {
start();
deepEqual(events, {
pan: true,
panstart: true,
panmove: true,
panright: true,
panend: true
});
});
});
asyncTest('recognize press', function() {
expect(1);
Simulator.gestures.press(el, null, function() {
start();
deepEqual(events, {
press: true
});
});
});
asyncTest('recognize swipe', function() {
expect(1);
Simulator.gestures.swipe(el, { duration: 300, deltaX: 400, deltaY: 0 }, function() {
start();
deepEqual(events, {
pan: true,
panstart: true,
panmove: true,
panright: true,
panend: true,
swipe: true,
swiperight: true
});
});
});
asyncTest('recognize pinch', function() {
expect(1);
Simulator.gestures.pinch(el, { duration: 500, scale: .5 }, function() {
start();
deepEqual(events, {
pinch: true,
pinchstart: true,
pinchmove: true,
pinchend: true,
pinchin: true
});
});
});
asyncTest('recognize children multitouch pinch', function() {
expect(1);
var el1 = utils.createHitArea(el),
el2 = utils.createHitArea(el);
Simulator.gestures.pinch([el1, el2], { duration: 500, scale: .5 }, function() {
start();
deepEqual(events, {
pinch: true,
pinchstart: true,
pinchmove: true,
pinchend: true,
pinchin: true
});
});
});
asyncTest('recognize parent-child multitouch pinch', function() {
expect(1);
var el1 = utils.createHitArea(el);
Simulator.gestures.pinch([el, el1], { duration: 100, scale: .5 }, function() {
start();
deepEqual(events, {
pinch: true,
pinchstart: true,
pinchmove: true,
pinchend: true,
pinchin: true
});
});
});
asyncTest('recognize rotate', function() {
expect(1);
Simulator.gestures.rotate(el, { duration: 500, scale: 1 }, function() {
start();
deepEqual(events, {
rotate: true,
rotatestart: true,
rotatemove: true,
rotateend: true
});
});
});
asyncTest('recognize multitouch rotate', function() {
expect(1);
var el1 = utils.createHitArea(el);
Simulator.gestures.rotate([el, el1], { duration: 500, scale: 1 }, function() {
start();
deepEqual(events, {
rotate: true,
rotatestart: true,
rotatemove: true,
rotateend: true
});
});
});
asyncTest('recognize rotate and pinch simultaneous', function() {
expect(1);
Simulator.gestures.pinchRotate(el, { duration: 500, scale: 2 }, function() {
start();
deepEqual(events, {
rotate: true,
rotatestart: true,
rotatemove: true,
rotateend: true,
pinch: true,
pinchstart: true,
pinchmove: true,
pinchend: true,
pinchout: true
});
});
});
asyncTest('don\'t recognize pan and swipe when moving down, when only horizontal is allowed', function() {
expect(1);
Simulator.gestures.swipe(el, { duration: 250, deltaX: 0, deltaZ: 200 }, function() {
start();
deepEqual(events, { });
});
});
asyncTest('don\'t recognize press if duration is too short.', function() {
expect(1);
Simulator.gestures.press(el, { duration: 240 });
setTimeout(function() {
start();
deepEqual(events, { tap: true }, 'Tap gesture has been recognized.');
}, 275);
});
asyncTest('don\'t recognize tap if duration is too long.', function() {
expect(1);
Simulator.gestures.tap(el, { duration: 255 });
setTimeout(function() {
start();
deepEqual(events, { press: true }, 'Press gesture has been recognized.');
}, 275);
});

View File

@@ -0,0 +1,187 @@
var el, el2,
hammer, hammer2;
module('Tests', {
setup: function() {
el = utils.createHitArea();
el2 = utils.createHitArea();
},
teardown: function() {
if (hammer) {
hammer.destroy();
hammer = null;
}
if (hammer2) {
hammer2.destroy();
hammer2 = null;
}
}
});
test('hammer shortcut', function() {
expect(2);
Hammer.defaults.touchAction = 'pan-y';
hammer = Hammer(el);
ok(hammer instanceof Hammer.Manager, 'returns an instance of Manager');
ok(hammer.touchAction.actions == Hammer.defaults.touchAction, 'set the default touchAction');
});
test('hammer shortcut with options', function() {
expect(2);
hammer = Hammer(el, {
touchAction: 'none'
});
ok(hammer instanceof Hammer.Manager, 'returns an instance of Manager');
ok(hammer.touchAction.actions == 'none', 'set the default touchAction');
});
/* Creating a hammer instance does not work on the same way
* when using Hammer or Hammer.Manager.
*
* This can confuse developers who read tests to use the library when doc is missing.
*/
test('Hammer and Hammer.Manager constructors work exactly on the same way.', function() {
expect(2);
hammer = new Hammer(el, {});
equal(Hammer.defaults.preset.length, hammer.recognizers.length);
hammer2 = new Hammer.Manager(el, {});
equal(0, hammer2.recognizers.length);
});
/* DOC to disable default recognizers should be added.
*
* - Hammer(el). IMO: Currently, well done.
* - Hammer(el, {}) . IMO: should disable default recognizers
* - Hammer(el, {recognizers: null}). IMO: now, it fails.
* - Hammer(el, {recognizers: []}). It works, but it is likely not intuitive.
*/
test('A Hammer instance can be setup to not having default recognizers.', function() {
expect(1);
hammer = new Hammer(el, { recognizers: false });
equal(0, hammer.recognizers.length);
});
/* The case was when I added a custom tap event which was added to the default
* recognizers, and my custom tap gesture wasn't working (I do not know exactly the reason),
* but removing the default recognizers solved the issue.
*/
test('Adding the same recognizer type should remove the old recognizer', function() {
expect(4);
hammer = new Hammer(el);
ok(!!hammer.get('tap'));
equal(7, hammer.recognizers.length);
var newTap = new Hammer.Tap({time: 1337});
hammer.add(newTap);
equal(7, hammer.recognizers.length);
equal(1337, hammer.get('tap').options.time);
});
/*
* Swipe gesture:
* - in this tests, it does not update input.velocity ( always 0)
* - does not fire swipeleft or swiperight events
*/
asyncTest('Swiping to the left should fire swipeleft event', function() {
expect(2);
hammer = new Hammer(el, {recognizers: []});
hammer.add(new Hammer.Swipe());
hammer.on('swipe swipeleft', function() {
ok(true);
});
Simulator.gestures.swipe(el, {pos: [300, 300], deltaY: 0, deltaX: -200}, function() {
start();
});
});
/*
* Input target change
*/
asyncTest('Should detect input while on other element', function() {
expect(1);
hammer = new Hammer(el, { inputTarget: document.body });
hammer.on('tap', function() {
ok(true);
});
Simulator.gestures.tap(document.body, null, function() {
start();
});
});
/* Hammer.Manager constructor accepts a "recognizers" option in which each
* element is an array representation of a Recognizer.
*/
test('Hammer.Manager accepts recognizers as arrays.', function() {
expect(4);
hammer = new Hammer.Manager(el, {
recognizers: [
[Hammer.Swipe],
[Hammer.Pinch],
[Hammer.Rotate],
[Hammer.Pan, { direction: Hammer.DIRECTION_UP }, ['swipe', 'pinch'], ['rotate']]
]
});
equal(4, hammer.recognizers.length);
var recognizerActual = hammer.recognizers[3];
equal(recognizerActual.options.direction, Hammer.DIRECTION_UP);
equal(2, Object.keys(recognizerActual.simultaneous).length);
equal(1, recognizerActual.requireFail.length);
});
/*
* Removing a recognizer which cannot be found would errantly remove the last recognizer in the
* manager's list.
*/
test('Remove non-existent recognizer.', function() {
expect(1);
hammer = new Hammer(el, {recognizers: []});
hammer.add(new Hammer.Swipe());
hammer.remove('tap');
equal(1, hammer.recognizers.length);
});
test('check whether Hammer.defaults.cssProps is restored', function() {
var beforeCssProps = {
userSelect: 'text',
touchSelect: 'grippers',
touchCallout: 'default',
contentZooming: 'chained',
userDrag: 'element',
tapHighlightColor: 'rgba(0, 1, 0, 0)'
};
var prop;
Hammer.each(Hammer.defaults.cssProps, function(value, name) {
prop = Hammer.prefixed(el.style, name);
if (prop) {
el.style[prop] = beforeCssProps[name];
}
});
hammer = Hammer(el);
hammer.destroy();
hammer = null;
Hammer.each(Hammer.defaults.cssProps, function(value, name) {
prop = Hammer.prefixed(el.style, name);
if (prop) {
equal(el.style[prop], beforeCssProps[name], "check if " + name + " is restored");
}
});
});

View File

@@ -0,0 +1,59 @@
var el, hammer, events;
var jQueryPluginPath = '../../node_modules/jquery-hammerjs/jquery.hammer.js';
module('jQuery plugin', {
setup: function() {
el = utils.createHitArea();
events = {};
},
teardown: function() {
hammer && hammer.destroy();
}
});
asyncTest('trigger pan with jQuery', function() {
expect(2);
$.getScript(jQueryPluginPath, function() {
jQuery(el).hammer();
jQuery(el).bind('panstart pan panmove panright panend', function(ev) {
if (ev.gesture) {
events[ev.type] = true;
}
});
Simulator.gestures.pan(el, { deltaX: 50, deltaY: 0 }, function() {
start();
deepEqual(events, {
pan: true,
panstart: true,
panmove: true,
panright: true,
panend: true
});
ok(jQuery(el).data('hammer') instanceof Hammer.Manager, 'data attribute refers to the instance');
});
});
});
asyncTest('trigger pan without jQuery should still work', function() {
expect(1);
var hammer = Hammer(el);
hammer.on('panstart pan panmove panright panend', function(ev) {
events[ev.type] = true;
});
Simulator.gestures.pan(el, { deltaX: 50, deltaY: 0 }, function() {
start();
deepEqual(events, {
pan: true,
panstart: true,
panmove: true,
panright: true,
panend: true
});
});
});

View File

@@ -0,0 +1,90 @@
var el, hammer;
var tripleTapCount = 0,
doubleTapCount = 0,
tapCount = 0;
module('Tap delay', {
setup: function() {
el = utils.createHitArea();
hammer = new Hammer(el, {recognizers: []});
var tap = new Hammer.Tap();
var doubleTap = new Hammer.Tap({event: 'doubleTap', taps: 2 });
var tripleTap = new Hammer.Tap({event: 'tripleTap', taps: 3 });
hammer.add([tripleTap, doubleTap, tap]);
tripleTap.recognizeWith([doubleTap, tap]);
doubleTap.recognizeWith(tap);
doubleTap.requireFailure(tripleTap);
tap.requireFailure([tripleTap, doubleTap]);
tripleTapCount = 0;
doubleTapCount = 0;
tapCount = 0;
hammer.on('tap', function() {
tapCount++;
});
hammer.on('doubleTap', function() {
doubleTapCount++;
});
hammer.on('tripleTap', function() {
tripleTapCount++;
});
},
teardown: function() {
hammer.destroy();
}
});
asyncTest('When a tripleTap is fired, doubleTap and Tap should not be recognized', function() {
expect(3);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(tripleTapCount, 1, 'one tripletap event');
equal(doubleTapCount, 0, 'no doubletap event');
equal(tapCount, 0, 'no singletap event');
}, 350);
});
asyncTest('When a doubleTap is fired, tripleTap and Tap should not be recognized', function() {
expect(3);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(tripleTapCount, 0);
equal(doubleTapCount, 1);
equal(tapCount, 0);
}, 350);
});
asyncTest('When a tap is fired, tripleTap and doubleTap should not be recognized', function() {
expect(3);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(tripleTapCount, 0);
equal(doubleTapCount, 0);
equal(tapCount, 1);
}, 350);
});

View File

@@ -0,0 +1,167 @@
var parent,
child,
hammerChild,
hammerParent;
module('Nested gesture recognizers (Tap Child + Pan Parent)', {
setup: function() {
parent = document.createElement('div');
child = document.createElement('div');
document.getElementById('qunit-fixture').appendChild(parent);
parent.appendChild(child);
hammerParent = new Hammer.Manager(parent, {
touchAction: 'none'
});
hammerChild = new Hammer.Manager(child, {
touchAction: 'none'
});
hammerChild.add(new Hammer.Tap());
hammerParent.add(new Hammer.Pan({threshold: 5, pointers: 1}));
},
teardown: function() {
hammerChild.destroy();
hammerParent.destroy();
}
});
test('Tap on the child', function() {
expect(1);
hammerChild.on('tap', function() {
ok(true);
});
hammerParent.on('tap', function() {
throw new Error('tap should not fire on parent');
});
utils.dispatchTouchEvent(child, 'start', 0, 10);
utils.dispatchTouchEvent(child, 'end', 0, 10);
});
test('Panning on the child should fire parent pan and should not fire child tap event', function() {
expect(1);
hammerChild.on('tap', function() {
throw new Error('tap should not fire on parent');
});
hammerParent.on('panend', function() {
ok(true);
});
utils.dispatchTouchEvent(child, 'start', 10, 0);
utils.dispatchTouchEvent(child, 'move', 20, 0);
utils.dispatchTouchEvent(child, 'end', 30, 0);
});
/*
// test (optional pointers validation)
test('Panning with one finger down on child, other on parent', function () {
expect(1);
var event,
touches;
hammerParent.on('panend', function () {
ok(true);
});
// one finger one child
utils.dispatchTouchEvent(child, 'start', 10, 0, 0);
utils.dispatchTouchEvent(parent, 'start', 12, 0, 1);
touches = [
{clientX: 20, clientY: 0, identifier: 0 },
{clientX: 20, clientY: 0, identifier: 1 }
];
event = document.createEvent('Event');
event.initEvent('touchmove', true, true);
event.touches = touches;
event.changedTouches = touches;
parent.dispatchEvent(event);
touches = [
{clientX: 30, clientY: 0, identifier: 0 },
{clientX: 30, clientY: 0, identifier: 1 }
];
event = document.createEvent('Event');
event.initEvent('touchend', true, true);
event.touches = touches;
event.changedTouches = touches;
parent.dispatchEvent(event);
});
*/
var pressPeriod = 600;
module('Nested gesture recognizers (Press Child + Pan Parent)', {
setup: function() {
parent = document.createElement('div');
child = document.createElement('div');
document.getElementById('qunit-fixture').appendChild(parent);
parent.appendChild(child);
hammerParent = new Hammer.Manager(parent, {
touchAction: 'none'
});
hammerChild = new Hammer.Manager(child, {
touchAction: 'none'
});
hammerChild.add(new Hammer.Press({time: pressPeriod}));
hammerParent.add(new Hammer.Pan({threshold: 5, pointers: 1}));
},
teardown: function() {
hammerChild.destroy();
hammerParent.destroy();
}
});
test('Press on the child', function() {
expect(1);
hammerChild.on('press', function() {
ok(true);
});
hammerParent.on('press', function() {
throw new Error('press should not fire on parent');
});
utils.dispatchTouchEvent(child, 'start', 0, 10);
stop();
setTimeout(function() {
start();
}, pressPeriod);
});
test('When Press is followed by Pan on the same element, both gestures are recognized', function() {
expect(2);
hammerChild.on('press', function() {
ok(true);
});
hammerParent.on('panend', function() {
ok(true);
});
utils.dispatchTouchEvent(child, 'start', 0, 10);
stop();
setTimeout(function() {
start();
utils.dispatchTouchEvent(child, 'move', 10, 10);
utils.dispatchTouchEvent(child, 'move', 20, 10);
utils.dispatchTouchEvent(child, 'move', 30, 10);
utils.dispatchTouchEvent(child, 'end', 30, 10);
}, pressPeriod);
});

View File

@@ -0,0 +1,56 @@
var parent,
child,
hammerChild,
hammerParent;
module('Propagation (Tap in Child and Parent)', {
setup: function() {
parent = document.createElement('div');
child = document.createElement('div');
document.getElementById('qunit-fixture').appendChild(parent);
parent.appendChild(child);
hammerParent = new Hammer.Manager(parent);
hammerChild = new Hammer.Manager(child);
hammerChild.add(new Hammer.Tap());
hammerParent.add(new Hammer.Tap());
},
teardown: function() {
hammerChild.destroy();
hammerParent.destroy();
}
});
test('Tap on the child, fires also the tap event to the parent', function() {
expect(2);
hammerChild.on('tap', function() {
ok(true);
});
hammerParent.on('tap', function() {
ok(true);
});
utils.dispatchTouchEvent(child, 'start', 0, 10);
utils.dispatchTouchEvent(child, 'end', 0, 10);
});
test('When tap on the child and the child stops the input event propagation, the tap event does not get fired in the parent', function() {
expect(1);
hammerChild.on('tap', function() {
ok(true);
});
hammerParent.on('tap', function() {
throw new Error('parent tap gesture should not be recognized');
});
child.addEventListener('touchend', function(ev) {
ev.stopPropagation();
});
utils.dispatchTouchEvent(child, 'start', 0, 10);
utils.dispatchTouchEvent(child, 'end', 0, 10);
});

View File

@@ -0,0 +1,111 @@
var el,
hammer,
pressPeriod = 200,
pressThreshold = 20,
pressCount = 0,
panStartCount = 0,
swipeCount = 0;
module('Require Failure ( Swipe & Press )', {
setup: function() {
el = utils.createHitArea();
hammer = new Hammer(el, {recognizers: []});
var swipe = new Hammer.Swipe({threshold: 1});
var press = new Hammer.Press({time: pressPeriod, threshold: pressThreshold});
hammer.add(swipe);
hammer.add(press);
swipe.recognizeWith(press);
press.requireFailure(swipe);
pressCount = 0;
swipeCount = 0;
hammer.on('press', function() {
pressCount++;
});
hammer.on('swipe', function() {
swipeCount++;
});
},
teardown: function() {
hammer.destroy();
}
});
asyncTest('When swipe does not recognize the gesture, a press gesture can be fired', function() {
expect(1);
utils.dispatchTouchEvent(el, 'start', 50, 50);
setTimeout(function() {
start();
equal(pressCount, 1);
}, pressPeriod + 100);
});
asyncTest('When swipe does recognize the gesture, a press gesture cannot be fired', function() {
expect(2);
Simulator.gestures.swipe(el, null, function() {
start();
ok(swipeCount > 0, 'swipe gesture should be recognizing');
equal(pressCount, 0, 'press gesture should not be recognized because swipe gesture is recognizing');
});
});
module('Require Failure ( Pan & Press )', {
setup: function() {
el = document.createElement('div');
document.body.appendChild(el);
hammer = new Hammer(el, {recognizers: []});
var pan = new Hammer.Pan({threshold: 1});
var press = new Hammer.Press({time: pressPeriod, threshold: pressThreshold});
hammer.add([pan, press]);
pan.recognizeWith(press);
press.requireFailure(pan);
pressCount = 0;
panStartCount = 0;
hammer.on('press', function() {
pressCount++;
});
hammer.on('panstart', function() {
panStartCount++;
});
},
teardown: function() {
document.body.removeChild(el);
hammer.destroy();
}
});
asyncTest('When pan does not recognize the gesture, a press gesture can be fired', function() {
expect(1);
utils.dispatchTouchEvent(el, 'start', 50, 50);
setTimeout(function() {
start();
equal(pressCount, 1);
}, pressPeriod + 100);
});
asyncTest('When pan recognizes the gesture, a press gesture cannot be fired', function() {
expect(2);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'move', 50 + pressThreshold / 4, 50);
setTimeout(function() {
start();
ok(panStartCount > 0, 'pan gesture should be recognizing');
equal(pressCount, 0, 'press gesture should not be recognized because pan gesture is recognizing');
}, pressPeriod + 100);
});

View File

@@ -0,0 +1,234 @@
var el,
hammer;
module('Simultaenous recognition', {
setup: function() {
el = utils.createHitArea()
},
teardown: function() {
hammer && hammer.destroy();
}
});
asyncTest('should pinch and pan simultaneously be recognized when enabled', function() {
expect(4);
var panCount = 0,
pinchCount = 0;
hammer = new Hammer.Manager(el, {
touchAction: 'none'
});
hammer.add(new Hammer.Pan({threshold: 5, pointers: 2}));
var pinch = new Hammer.Pinch({ threshold: 0, pointers: 2});
hammer.add(pinch);
pinch.recognizeWith(hammer.get('pan'));
hammer.on('panend', function() {
panCount++;
});
hammer.on('pinchend', function() {
pinchCount++;
});
var executeGesture = function(cb) {
var event, touches;
touches = [
{clientX: 0, clientY: 10, identifier: 0, target: el },
{clientX: 10, clientY: 10, identifier: 1, target: el }
];
event = document.createEvent('Event');
event.initEvent('touchstart', true, true);
event.touches = touches;
event.targetTouches = touches;
event.changedTouches = touches;
el.dispatchEvent(event);
setTimeout(function() {
touches = [
{clientX: 10, clientY: 20, identifier: 0, target: el },
{clientX: 20, clientY: 20, identifier: 1, target: el }
];
event = document.createEvent('Event');
event.initEvent('touchmove', true, true);
event.touches = touches;
event.targetTouches = touches;
event.changedTouches = touches;
el.dispatchEvent(event);
}, 100);
setTimeout(function() {
touches = [
{clientX: 20, clientY: 30, identifier: 0, target: el },
{clientX: 40, clientY: 30, identifier: 1, target: el }
];
event = document.createEvent('Event');
event.initEvent('touchmove', true, true);
event.touches = touches;
event.targetTouches = touches;
event.changedTouches = touches;
el.dispatchEvent(event);
event = document.createEvent('Event');
event.initEvent('touchend', true, true);
event.touches = touches;
event.targetTouches = touches;
event.changedTouches = touches;
el.dispatchEvent(event);
cb();
}, 200);
};
// 2 gesture will be recognized
executeGesture(function() {
equal(panCount, 1);
equal(pinchCount, 1);
pinch.dropRecognizeWith(hammer.get('pan'));
// only the pan gesture will be recognized
executeGesture(function() {
equal(panCount, 2);
equal(pinchCount, 1);
start();
});
});
});
test('the first gesture should block the following gestures (Tap & DoubleTap)', function() {
expect(4);
var tapCount = 0,
doubleTapCount = 0;
hammer = new Hammer.Manager(el, {
touchAction: 'none'
});
var tap = new Hammer.Tap();
var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2});
hammer.add(tap);
hammer.add(doubleTap);
hammer.on('tap', function() {
tapCount++;
});
hammer.on('doubletap', function() {
doubleTapCount++;
});
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(tapCount, 2, 'on a double tap gesture, the tap gesture is recognized twice');
equal(doubleTapCount, 0, 'double tap gesture is not recognized because the prior tap gesture does not recognize it simultaneously');
doubleTap.recognizeWith(hammer.get('tap'));
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(tapCount, 4);
equal(doubleTapCount, 1, 'when the tap gesture is configured to work simultaneously, tap & doubleTap can be recognized simultaneously');
});
test('when disabled, the first gesture should not block gestures (Tap & DoubleTap )', function() {
expect(4);
var tapCount = 0,
doubleTapCount = 0;
hammer = new Hammer.Manager(el, {
touchAction: 'none'
});
var tap = new Hammer.Tap();
var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2});
hammer.add(tap);
hammer.add(doubleTap);
hammer.on('tap', function() {
tapCount++;
});
hammer.on('doubletap', function() {
doubleTapCount++;
});
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(tapCount, 2, 'on a double tap gesture, the tap gesture is recognized twice');
equal(doubleTapCount, 0, 'double tap gesture is not recognized because the prior tap gesture does not recognize it simultaneously');
hammer.get('tap').set({ enable: false });
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(tapCount, 2, 'tap gesture should not be recognized when the recognizer is disabled');
equal(doubleTapCount, 1, 'when the tap gesture is disabled, doubleTap can be recognized');
});
test('the first gesture should block the following gestures (DoubleTap & Tap)', function() {
expect(4);
var tapCount = 0,
doubleTapCount = 0;
hammer = new Hammer.Manager(el, {
touchAction: 'none'
});
var tap = new Hammer.Tap();
var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2});
hammer.add(doubleTap);
hammer.add(tap);
hammer.on('tap', function() {
tapCount++;
});
hammer.on('doubletap', function() {
doubleTapCount++;
});
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(doubleTapCount, 1, 'double tap is recognized');
equal(tapCount, 1, 'tap is detected, the doubletap is only catched by the doubletap recognizer');
// doubletap and tap together
doubleTap.recognizeWith(hammer.get('tap'));
doubleTapCount = 0;
tapCount = 0;
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(doubleTapCount, 1);
equal(tapCount, 2, 'when the tap gesture is configured to work simultaneously, tap & doubleTap can be recognized simultaneously');
});

View File

@@ -0,0 +1,164 @@
module('utils');
// for the tests, all hammer properties and methods of Hammer are exposed to window.$H
test('get/set prefixed util', function() {
ok(_.isUndefined($H.prefixed(window, 'FakeProperty')), 'non existent property returns undefined');
window.webkitFakeProperty = 1337;
ok($H.prefixed(window, 'FakeProperty') == 'webkitFakeProperty', 'existent prefixed property returns the prefixed name');
delete window.webkitFakeProperty;
});
test('fnBind', function() {
var context = { a: true };
$H.bindFn(function(b) {
ok(this.a === true, 'bindFn scope');
ok(b === 123, 'bindFn argument');
}, context)(123);
});
test('Inherit objects', function() {
function Base() {
this.name = true;
}
function Child() {
Base.call(this);
}
$H.inherit(Child, Base, {
newMethod: function() {
}
});
var inst = new Child();
ok(inst.name == true, 'child has extended from base');
ok(inst.newMethod, 'child has a new method');
ok(Child.prototype.newMethod, 'child has a new prototype method');
ok(inst instanceof Child, 'is instanceof Child');
ok(inst instanceof Base, 'is instanceof Base');
ok(inst._super === Base.prototype, '_super is ref to prototype of Base');
});
test('toArray', function() {
ok(_.isArray($H.toArray({ 0: true, 1: 'second', length: 2 })), 'converted an array-like object to an array');
ok(_.isArray($H.toArray([true, true])), 'array stays an array');
});
test('inArray', function() {
ok($H.inArray([1, 2, 3, 4, 'hammer'], 'hammer') === 4, 'found item and returned the index');
ok($H.inArray([1, 2, 3, 4, 'hammer'], 'notfound') === -1, 'not found an item and returned -1');
ok($H.inArray([
{id: 2},
{id: 24}
], '24', 'id') === 1, 'find by key and return the index');
ok($H.inArray([
{id: 2},
{id: 24}
], '22', 'id') === -1, 'not found by key and return -1');
});
test('splitStr', function() {
deepEqual($H.splitStr(' a b c d '), ['a', 'b', 'c', 'd'], 'str split valid');
});
test('uniqueArray', function() {
deepEqual($H.uniqueArray([
{id: 1},
{id: 2},
{id: 2}
], 'id'), [
{id: 1},
{id: 2}
], 'remove duplicate ids')
});
test('boolOrFn', function() {
equal($H.boolOrFn(true), true, 'Passing an boolean');
equal($H.boolOrFn(false), false, 'Passing an boolean');
equal($H.boolOrFn(function() {
return true;
}), true, 'Passing an boolean');
equal($H.boolOrFn(1), true, 'Passing an integer');
});
test('hasParent', function() {
var parent = document.createElement('div'),
child = document.createElement('div');
document.body.appendChild(parent);
parent.appendChild(child);
equal($H.hasParent(child, parent), true, 'Found parent');
equal($H.hasParent(parent, child), false, 'Not in parent');
document.body.removeChild(parent);
});
test('each', function() {
var object = { hi: true };
var array = ['a', 'b', 'c'];
var loop;
loop = false;
$H.each(object, function(value, key) {
if (key == 'hi' && value === true) {
loop = true;
}
});
ok(loop, 'object loop');
loop = 0;
$H.each(array, function(value, key) {
if (value) {
loop++;
}
});
ok(loop == 3, 'array loop');
loop = 0;
array.forEach = null;
$H.each(array, function(value, key) {
if (value) {
loop++;
}
});
ok(loop == 3, 'array loop without Array.forEach');
});
test('assign', function() {
expect(2);
deepEqual(
$H.assign(
{a: 1, b: 3},
{b: 2, c: 3}
),
{a: 1, b: 2, c: 3},
'Simple extend'
);
var src = { foo: true };
var dest = $H.assign({}, src);
src.foo = false;
deepEqual(dest, {foo: true}, 'Clone reference');
});
test('test add/removeEventListener', function() {
function handleEvent() {
ok(true, 'triggered event');
}
expect(2);
$H.addEventListeners(window, 'testEvent1 testEvent2 ', handleEvent);
utils.triggerDomEvent(window, 'testEvent1');
utils.triggerDomEvent(window, 'testEvent2');
$H.removeEventListeners(window, ' testEvent1 testEvent2 ', handleEvent);
utils.triggerDomEvent(window, 'testEvent1');
utils.triggerDomEvent(window, 'testEvent2');
});