CB-9365 Add support for 'vibrateWithPattern' to Windows Phone 8.1 / Windows 10

Added manual test for `repeat`
diff --git a/README.md b/README.md
index b7117e5..38a3742 100644
--- a/README.md
+++ b/README.md
@@ -111,10 +111,6 @@
 
 - vibrate(pattern) falls back on vibrate with default duration
 
-####Windows Quirks
-
-- vibrate(pattern) falls back on vibrate with default duration
-
 ###Cancel vibration (not supported in iOS)
 
 Immediately cancels any currently running vibration.
diff --git a/src/windows/VibrationProxy.js b/src/windows/VibrationProxy.js
index 3fb954b..0c01eb1 100644
--- a/src/windows/VibrationProxy.js
+++ b/src/windows/VibrationProxy.js
@@ -41,7 +41,117 @@
     }
 }
 
+/** 
+ * @typedef patternParsingResult
+ * @type {Object}
+ * @property {Array} result.parsed - Array with parsed integers
+ * @property {Boolean} result.passed - false in case of parsing error
+ * @property {*} result.failedItem - The item, which could not be parsed
+ */
+
+/**
+ * Tries to convert pattern values to int
+ * @param  {Array} pattern Array of delays
+ * @returns {patternParsingResult} result
+ */
+function tryParsePatternValues(pattern) {
+    var passed = true, failedItem;
+
+    pattern = pattern.map(function (item) {
+        var num = parseInt(item, 10);
+        if (isNaN(num)) {
+            failedItem = item;
+            passed = false;
+        }
+
+        return num;
+    });
+
+    return {
+        parsed: pattern,
+        passed: passed,
+        failedItem: failedItem
+    };
+}
+
+/** 
+ * @typedef checkPatternReqsResult
+ * @type {Object}
+ * @property {Array} result.patternParsingResult - Array with parsed integers
+ * @property {Boolean} result.passed - true if all params are OK
+ */
+
+/**
+ * Checks params for vibrateWithPattern function
+ * @return {checkPatternReqsResult}
+ */
+function checkPatternReqs(args, fail) {
+    var patternParsingResult = tryParsePatternValues(args[0]);
+    var repeat = args[1];
+    var passed = true, errMsg = '';
+
+    if (!patternParsingResult.passed) {
+        errMsg += 'Could not parse ' + patternParsingResult.failedItem + ' in the vibration pattern';
+        passed = false;
+    }
+
+    if (repeat !== -1 && (repeat < 0 || repeat > args[0].length - 1)) {
+        errMsg += '\nrepeat parameter is out of range: ' + repeat;
+        passed = false;
+    }
+
+    if (!passed) {
+        console.error(errMsg);
+        fail && fail(errMsg);
+    }
+
+    return {
+        passed: passed,
+        patternParsingResult: patternParsingResult
+    };
+}
+
+/**
+ * vibrateWithPattern with `repeat` support
+ * @param  {Array} patternArr Full pattern array
+ * @param  {Boolean} shouldRepeat Indication on whether the vibration should be cycled
+ * @param  {Function} fail Fail callback
+ * @param  {Array} patternCycle Cycled part of the pattern array
+ * @return {Promise} Promise chaining single vibrate/pause actions
+ */
+function vibratePattern(patternArr, shouldRepeat, fail, patternCycle) {
+    return patternArr.reduce(function (previousValue, currentValue, index) {
+        if (index % 2 === 0) {
+            return previousValue.then(function () {
+                module.exports.vibrate(function () { }, function (err) {
+                    console.error(err);
+                    fail && fail(err);
+                }, [currentValue]);
+
+                if (index === patternArr.length - 1 && shouldRepeat) {
+                    return WinJS.Promise.timeout(currentValue).then(function () {
+                        return vibratePattern(patternCycle, true, fail, patternCycle);
+                    });
+                } else {
+                    return WinJS.Promise.timeout(currentValue);
+                }
+            });
+        } else {
+            return previousValue.then(function () {
+                if (index === patternArr.length - 1 && shouldRepeat) {
+                    return WinJS.Promise.timeout(currentValue).then(function () {
+                        return vibratePattern(patternCycle, true, fail, patternCycle);
+                    });
+                } else {
+                    return WinJS.Promise.timeout(currentValue);
+                }
+            });
+        }
+    }, WinJS.Promise.as());
+}
+
 var DEFAULT_DURATION = 200;
+var patternChainPromise;
 
 var VibrationDevice = (Windows.Phone && Windows.Phone.Devices && Windows.Phone.Devices.Notification && Windows.Phone.Devices.Notification.VibrationDevice && Windows.Phone.Devices.Notification.VibrationDevice);
 if (VibrationDevice) {
@@ -60,17 +170,34 @@
                 fail(e);
             }
         }, 
-        vibrateWithPattern: function(success, fail, args) {
-            // TODO: Implement with setTimeout.
-            fail('"vibrateWithPattern" is unsupported by this platform.');
+        vibrateWithPattern: function (success, fail, args) {
+            // Cancel current vibrations first
+            module.exports.cancelVibration(function () {
+                var checkReqsResult = checkPatternReqs(args, fail);
+                if (!checkReqsResult.passed) {
+                    return;
+                }
+
+                var pattern = checkReqsResult.patternParsingResult.parsed;
+                var repeatFromIndex = args[1];
+                var shouldRepeat = (repeatFromIndex !== -1);
+                var patternCycle;
+
+                if (shouldRepeat) {
+                    patternCycle = pattern.slice(repeatFromIndex);
+                }
+
+                patternChainPromise = vibratePattern(pattern, shouldRepeat, fail, patternCycle);
+            }, fail);
         },
         cancelVibration: function(success, fail, args) {
             try {
+                patternChainPromise && patternChainPromise.cancel();
                 VibrationDevice.getDefault().cancel();
-                success();
+                success && success();
             }
             catch (e) {
-                fail(e);
+                fail && fail(e);
             }
         }
     };
@@ -85,24 +212,24 @@
             tryDoAction("vibrate", success, fail, [DEFAULT_DURATION], Vibration.Vibration.vibrate);
         },
 
-        cancelVibration: function(success, fail, args) {
+        cancelVibration: function (success, fail, args) {
             tryDoAction("cancelVibration", success, fail, args, Vibration.Vibration.cancelVibration);
         }
     };
 } else {
     // code paths where no vibration mechanism is present
     module.exports = {
-        vibrate: function (a, fail) {
-            fail('"vibrate" is unsupported by this device.');
+        vibrate: function (success, fail) {
+            fail && fail('"vibrate" is unsupported by this device.');
         },
         vibrateWithPattern: function (success, fail, args) {
-            fail('"vibrateWithPattern" is unsupported by this device.');
+            fail && fail('"vibrateWithPattern" is unsupported by this device.');
         },
 
-        cancelVibration: function(success, fail, args) {
-            success();
+        cancelVibration: function (success, fail, args) {
+            success && success();
         }
-    }
+    };
 }
 
 require("cordova/exec/proxy").add("Vibration", module.exports);
diff --git a/tests/tests.js b/tests/tests.js
index 79aff30..88dbc45 100644
--- a/tests/tests.js
+++ b/tests/tests.js
@@ -67,6 +67,13 @@
         logMessage("navigator.notification.vibrateWithPattern([1000, 3000, 2000, 5000])", "green");
     };
 
+    //old vibrate with pattern with repeat call
+    var vibrateWithPatternOldWithRepeat = function(){
+        clearLog();
+        navigator.notification.vibrateWithPattern([1000, 3000, 2000, 5000], 2);
+        logMessage("navigator.notification.vibrateWithPattern([1000, 3000, 2000, 5000], 2)", "green");
+    };
+
     //old cancel vibrate call
     var cancelOld = function(){
         clearLog();
@@ -157,6 +164,8 @@
         'Expected result: Vibrate once for 2.5 seconds.' +
         '<p/> <div id="vibrateWithPattern_old"></div>' +
         'Expected result: Pause for 1s, vibrate for 3s, pause for 2s, vibrate for 5s.' +
+        '<p/> <div id="vibrateWithPatternRepeat_old"></div>' +
+        'Expected result: Pause for 1s, vibrate for 3s, [pause for 2s, vibrate for 5s.], repeat [steps]' +
         '<p/> <div id="cancelVibrate_old"></div>' +
         'Expected result: Press once to initiate vibrate for 60 seconds. Press again to cancel vibrate immediately.' +
         '<p/> <div id="cancelVibrateWithPattern_old"></div>' +
@@ -191,6 +200,11 @@
         vibrateWithPatternOld();
     }, 'vibrateWithPattern_old');
 
+    //vibrate with pattern with repeat with old call
+    createActionButton('* Vibrate with a pattern with repeat (Old)', function () {
+        vibrateWithPatternOldWithRepeat();
+    }, 'vibrateWithPatternRepeat_old');
+
     //cancel vibrate with old call
     createActionButton('* Cancel vibration (Old)', function() {