diff --git a/test/coverage.html b/test/coverage.html
index 81ffe579..b04d5fda 100644
--- a/test/coverage.html
+++ b/test/coverage.html
@@ -53,6 +53,7 @@
     <!-- Helpers -->
     <script src="/test/helpers/legacy.mouse.shim.js"></script>
     <script src="/test/helpers/test.js"></script>
+    <script src="/test/helpers/touch.js"></script>
 
     <!-- Modules -->
     <!-- Polyfill must be inserted first because it is testing functions
diff --git a/test/helpers/touch.js b/test/helpers/touch.js
new file mode 100644
index 00000000..ad86ceaa
--- /dev/null
+++ b/test/helpers/touch.js
@@ -0,0 +1,134 @@
+/* global TouchUtil, $ */
+
+(function () {
+
+    var touches,
+        identifier,
+        target;
+      
+    // ----------
+    window.TouchUtil = {
+        reset: function () {
+            touches = [];
+            identifier = 0;
+        },
+
+        initTracker: function ( tracker ) {
+            // for testing in other touch-enabled browsers
+            if ( !('ontouchstart' in window) ) {
+                tracker.setTracking( false );
+                OpenSeadragon.MouseTracker.subscribeEvents.push( 'touchstart', 'touchend' );
+                tracker.setTracking( true );
+            }
+
+            target = tracker.element;
+        },
+
+        resetTracker: function ( tracker ) {
+            // for testing in other touch-enabled browsers
+            if ( !('ontouchstart' in window) ) {
+                tracker.setTracking( false );
+                ['touchstart', 'touchend'].forEach(function ( type ) { 
+                    var index = OpenSeadragon.MouseTracker.subscribeEvents.indexOf( type );
+                    if ( index > -1 ) {
+                      OpenSeadragon.MouseTracker.subscribeEvents.splice( index, 1 );
+                    }
+                });
+                tracker.setTracking( true );
+            }
+
+            target = null;
+        },
+
+        start: function () {
+            var touch,
+                event,
+                newTouches = [];
+
+            for ( var i = 0; i < arguments.length; i++ ) {
+                touch = createTouch(
+                    target.offsetLeft + arguments[ i ][ 0 ],
+                    target.offsetTop  + arguments[ i ][ 1 ]
+                );
+
+                touches.push( touch );
+                newTouches.push( touch );
+            }
+
+            event = createTouchEvent( 'touchstart', newTouches );
+            target.dispatchEvent( event );
+            return newTouches.length === 1 ? newTouches[ 0 ] : newTouches;
+        },
+
+        end: function ( changedTouches ) {
+            if ( !$.isArray( changedTouches ) ) {
+                changedTouches = [ changedTouches ];
+            }
+
+            var event;
+            touches = touches.filter(function ( touch ) {
+                return changedTouches.indexOf( touch ) === -1;
+            });
+
+            event = createTouchEvent( 'touchend', changedTouches );
+            target.dispatchEvent( event );
+        }
+
+    };
+
+    // ----------
+    function createTouch( x, y ) {
+        try {
+            // new spec
+            return new Touch({
+                identifier: identifier++,
+                target: target,
+                pageX: target.offsetLeft + x,
+                pageY: target.offsetTop + y
+            } );
+        } catch (e) {
+            // legacy
+            return document.createTouch( window, target, identifier++, x, y, x, y );
+        }
+    }
+
+    function createTouchList( touches ) {
+        // legacy
+        return document.createTouchList.apply( document, touches );
+    }
+
+    function createTouchEvent( type, changedTouches ) {
+        try {
+            // new spec
+            return new TouchEvent( type, {
+                view: window,
+                bubbles: true,
+                cancelable: true,
+                touches: touches,
+                targetTouches: touches,
+                changedTouches: changedTouches
+            } );
+        } catch (e) {
+            // legacy
+            var touchEvent = document.createEvent( 'TouchEvent' );
+            var touch1 = changedTouches[ 0 ];
+            touchEvent.initTouchEvent(
+                createTouchList( touches ),         // touches
+                createTouchList( touches ),         // targetTouches
+                createTouchList( changedTouches ),  // changedTouches
+                type,                               // type
+                window,                             // view
+                touch1.screenX,                     // screenX
+                touch1.screenY,                     // screenY
+                touch1.clientX,                     // clientX
+                touch1.clientY,                     // clientY
+                false,                              // ctrlKey
+                false,                              // altKey
+                false,                              // shiftKey
+                false                               // metaKey
+            );
+            return touchEvent;
+        }
+    }
+
+})();
diff --git a/test/modules/events.js b/test/modules/events.js
index 2d435fc0..30dc28a2 100644
--- a/test/modules/events.js
+++ b/test/modules/events.js
@@ -1,4 +1,4 @@
-/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog */
+/* global module, asyncTest, $, ok, equal, notEqual, start, test, TouchUtil, Util, testLog */
 
 (function () {
     var viewer;
@@ -677,6 +677,82 @@
         viewer.open( '/test/data/testpattern.dzi' );
     } );
 
+    // ----------
+    if ('TouchEvent' in window) {
+        asyncTest( 'MouseTracker: touch events', function () {
+            var $canvas = $( viewer.element ).find( '.openseadragon-canvas' ).not( '.navigator .openseadragon-canvas' ),
+                tracker = viewer.innerTracker,
+                touches;
+
+            var reset = function () {
+                touches = [];
+                TouchUtil.reset();
+            };
+
+            var assessTouchExpectations = function ( expected ) {
+                var pointersList = tracker.getActivePointersListByType( 'touch' );
+                if ('captureCount' in expected) {
+                    equal( pointersList.captureCount, expected.captureCount, expected.description + 'Pointer capture count matches expected (' + expected.captureCount + ')' );
+                }
+                if ('contacts' in expected) {
+                    equal( pointersList.contacts, expected.contacts, expected.description + 'Pointer contact count matches expected (' + expected.contacts + ')' );
+                }
+                if ('trackedPointers' in expected) {
+                    equal( pointersList.getLength(), expected.trackedPointers, expected.description + 'Tracked pointer count matches expected (' + expected.trackedPointers + ')' );
+                }
+            };
+
+            var onOpen = function ( event ) {
+                viewer.removeHandler( 'open', onOpen );
+
+                TouchUtil.initTracker( tracker );
+
+                // start-end-end (multi-touch start event)
+                reset();
+                touches = TouchUtil.start( [0,0], [20,20] );
+                assessTouchExpectations({
+                    description:        'start-end-end (multi-touch start event) [capture]:  ',
+                    captureCount:       2,
+                    contacts:           2,
+                    trackedPointers:    2
+                });
+                TouchUtil.end( touches[1] );
+                TouchUtil.end( touches[0] );
+                assessTouchExpectations({
+                    description:        'start-end-end (multi-touch start event) [release]:  ',
+                    captureCount:       0,
+                    contacts:           0,
+                    trackedPointers:    0
+                });
+
+                // start-start-end (multi-touch end event)
+                reset();
+                touches.push( TouchUtil.start([0, 0]) );
+                touches.push( TouchUtil.start([20, 20]) );
+                assessTouchExpectations({
+                    description:        'start-start-end (multi-touch end event) [capture]:  ',
+                    captureCount:       2,
+                    contacts:           2,
+                    trackedPointers:    2
+                });
+                TouchUtil.end( touches );
+                assessTouchExpectations({
+                    description:        'start-start-end (multi-touch end event) [release]:  ',
+                    captureCount:       0,
+                    contacts:           0,
+                    trackedPointers:    0
+                });
+
+                TouchUtil.resetTracker( tracker );
+                viewer.close();
+                start();
+            };
+
+            viewer.addHandler( 'open', onOpen );
+            viewer.open( '/test/data/testpattern.dzi' );
+        } );
+    }
+
     // ----------
     asyncTest('Viewer: preventDefaultAction', function() {
         var $canvas = $(viewer.element).find('.openseadragon-canvas')
diff --git a/test/test.html b/test/test.html
index 6a42284f..74495ff6 100644
--- a/test/test.html
+++ b/test/test.html
@@ -17,6 +17,7 @@
     <script src="/build/openseadragon/openseadragon.js"></script>
     <script src="/test/helpers/legacy.mouse.shim.js"></script>
     <script src="/test/helpers/test.js"></script>
+    <script src="/test/helpers/touch.js"></script>
 
     <!-- Polyfill must be inserted first because it is testing functions
          reassignments which could be done by other test. -->