From 25fa7f8338941b65b6ca3f0c774b45547d5af80f Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Mon, 24 Sep 2018 02:47:47 +0100 Subject: [PATCH] sinonjs: Update from 1.17.3 to 1.17.7 Upstream Sinon.JS no longer supports the pre-2.x v1.17 branch anymore but there have been a few post-2.0 releases to address bugs in v1.17 that we can still benefit from. https://github.com/sinonjs/sinon/blob/v1.17.7/Changelog.txt Notable changes include: * Fix Blob feature test to support running tests on Safari 9. * Avoid calls to object-local 'hasOwnProperty'. * Improve error messages and stricter signature/type checks so that debugging code is easier when things go wrong. * Fix various gaps in the XHR mock. * Misc fixes for Node.js support. Also remove outdated comment about ie-hacks from Sinon 1.15. Change-Id: I66d1b461465b92798ad7eb2efcf4df2731cc78a4 --- maintenance/resources/foreign-resources.yaml | 6 +- .../lib/sinonjs/{sinon-1.17.3.js => sinon.js} | 422 +++++++++++++----- tests/qunit/QUnitTestResources.php | 9 +- 3 files changed, 320 insertions(+), 117 deletions(-) rename resources/lib/sinonjs/{sinon-1.17.3.js => sinon.js} (94%) diff --git a/maintenance/resources/foreign-resources.yaml b/maintenance/resources/foreign-resources.yaml index c84d6467ae..0f1eeacfe3 100644 --- a/maintenance/resources/foreign-resources.yaml +++ b/maintenance/resources/foreign-resources.yaml @@ -144,6 +144,6 @@ qunitjs: sinonjs: type: file - src: https://sinonjs.org/releases/sinon-1.17.3.js - integrity: sha384-8+RlaM2FW7qMqjxpM5NTVM0y6sTY+vTi/AHnk7Fd7NHjBye9sVxxsMjyxVJnPBtU - dest: sinon-1.17.3.js + src: https://sinonjs.org/releases/sinon-1.17.7.js + integrity: sha384-wR63Jwy75KqwBfzCmXd6gYws6uj3qV/XMAybzXrkEYGYG3AQ58ZWwr1fVpkHa5e8 + dest: sinon.js diff --git a/resources/lib/sinonjs/sinon-1.17.3.js b/resources/lib/sinonjs/sinon.js similarity index 94% rename from resources/lib/sinonjs/sinon-1.17.3.js rename to resources/lib/sinonjs/sinon.js index d77b317587..cf2f94efeb 100644 --- a/resources/lib/sinonjs/sinon-1.17.3.js +++ b/resources/lib/sinonjs/sinon.js @@ -1,5 +1,5 @@ /** - * Sinon.JS 1.17.3, 2016/01/27 + * Sinon.JS 1.17.7, 2017/02/15 * * @author Christian Johansen (christian@cjohansen.no) * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS @@ -179,6 +179,33 @@ } } + function isSet(val) { + if (typeof Set !== 'undefined' && val instanceof Set) { + return true; + } + } + + function isSubset(s1, s2, compare) { + var values1 = Array.from(s1); + var values2 = Array.from(s2); + + for (var i = 0; i < values1.length; i++) { + var includes = false; + + for (var j = 0; j < values2.length; j++) { + if (compare(values2[j], values1[i])) { + includes = true; + break; + } + } + + if (!includes) { + return false; + } + } + + return true; + } /** * @name samsam.deepEqual @@ -289,6 +316,14 @@ } } + if (isSet(obj1) || isSet(obj2)) { + if (!isSet(obj1) || !isSet(obj2) || obj1.size !== obj2.size) { + return false; + } + + return isSubset(obj1, obj2, deepEqual); + } + var key, i, l, // following vars are used for the cyclic logic value1, value2, @@ -361,15 +396,14 @@ }(obj1, obj2, '$1', '$2')); } - var match; - - function arrayContains(array, subset) { + function arrayContains(array, subset, compare) { if (subset.length === 0) { return true; } var i, l, j, k; for (i = 0, l = array.length; i < l; ++i) { - if (match(array[i], subset[0])) { + if (compare(array[i], subset[0])) { for (j = 0, k = subset.length; j < k; ++j) { - if (!match(array[i + j], subset[j])) { return false; } + if ((i + j) >= l) { return false; } + if (!compare(array[i + j], subset[j])) { return false; } } return true; } @@ -384,7 +418,7 @@ * * Compare arbitrary value ``object`` with matcher. */ - match = function match(object, matcher) { + function match(object, matcher) { if (matcher && typeof matcher.test === "function") { return matcher.test(object); } @@ -416,8 +450,12 @@ return object === null; } + if (isSet(object)) { + return isSubset(matcher, object, match); + } + if (getClass(object) === "Array" && getClass(matcher) === "Array") { - return arrayContains(object, matcher); + return arrayContains(object, matcher, match); } if (matcher && typeof matcher === "object") { @@ -444,7 +482,7 @@ throw new Error("Matcher was not a string, a number, a " + "function, a boolean or an object"); - }; + } return { isArguments: isArguments, @@ -669,34 +707,28 @@ return Formatio.prototype; }); -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.lolex=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1; + // Make properties writable in IE, as per // http://www.adequatelygood.com/Replacing-setTimeout-Globally.html - // JSLint being anal - var glbl = global; - - global.setTimeout = glbl.setTimeout; - global.clearTimeout = glbl.clearTimeout; - global.setInterval = glbl.setInterval; - global.clearInterval = glbl.clearInterval; - global.Date = glbl.Date; + if (isRunningInIE) { + global.setTimeout = global.setTimeout; + global.clearTimeout = global.clearTimeout; + global.setInterval = global.setInterval; + global.clearInterval = global.clearInterval; + global.Date = global.Date; + } // setImmediate is not a standard function // avoid adding the prop to the window object if not present - if('setImmediate' in global) { - global.setImmediate = glbl.setImmediate; - global.clearImmediate = glbl.clearImmediate; + if (global.setImmediate !== undefined) { + global.setImmediate = global.setImmediate; + global.clearImmediate = global.clearImmediate; } // node expects setTimeout/setInterval to return a fn object w/ .ref()/.unref() @@ -706,6 +738,7 @@ var NOOP = function () { return undefined; }; var timeoutResult = setTimeout(NOOP, 0); var addTimerReturnsObject = typeof timeoutResult === "object"; + var hrtimePresent = (global.process && typeof global.process.hrtime === "function"); clearTimeout(timeoutResult); var NativeDate = Date; @@ -742,6 +775,20 @@ return ms * 1000; } + /** + * Floor function that also works for negative numbers + */ + function fixedFloor(n) { + return (n >= 0 ? Math.floor(n) : Math.ceil(n)); + } + + /** + * % operator that also works for negative numbers + */ + function fixedModulo(n, m) { + return ((n % m) + m) % m; + } + /** * Used to grok the `now` parameter to createClock. */ @@ -800,22 +847,22 @@ // Defensive and verbose to avoid potential harm in passing // explicit undefined when user does not pass argument switch (arguments.length) { - case 0: - return new NativeDate(ClockDate.clock.now); - case 1: - return new NativeDate(year); - case 2: - return new NativeDate(year, month); - case 3: - return new NativeDate(year, month, date); - case 4: - return new NativeDate(year, month, date, hour); - case 5: - return new NativeDate(year, month, date, hour, minute); - case 6: - return new NativeDate(year, month, date, hour, minute, second); - default: - return new NativeDate(year, month, date, hour, minute, second, ms); + case 0: + return new NativeDate(ClockDate.clock.now); + case 1: + return new NativeDate(year); + case 2: + return new NativeDate(year, month); + case 3: + return new NativeDate(year, month, date); + case 4: + return new NativeDate(year, month, date, hour); + case 5: + return new NativeDate(year, month, date, hour, minute); + case 6: + return new NativeDate(year, month, date, hour, minute, second); + default: + return new NativeDate(year, month, date, hour, minute, second, ms); } } @@ -833,7 +880,7 @@ timer.id = uniqueTimerId++; timer.createdAt = clock.now; - timer.callAt = clock.now + (timer.delay || (clock.duringTick ? 1 : 0)); + timer.callAt = clock.now + (parseInt(timer.delay) || (clock.duringTick ? 1 : 0)); clock.timers[timer.id] = timer; @@ -849,6 +896,7 @@ } + /* eslint consistent-return: "off" */ function compareTimers(a, b) { // Sort first by absolute timing if (a.callAt < b.callAt) { @@ -920,6 +968,22 @@ return timer; } + function lastTimer(clock) { + var timers = clock.timers, + timer = null, + id; + + for (id in timers) { + if (timers.hasOwnProperty(id)) { + if (!timer || compareTimers(timer, timers[id]) === -1) { + timer = timers[id]; + } + } + } + + return timer; + } + function callTimer(clock, timer) { var exception; @@ -933,6 +997,7 @@ if (typeof timer.func === "function") { timer.func.apply(null, timer.args); } else { + /* eslint no-eval: "off" */ eval(timer.func); } } catch (e) { @@ -954,11 +1019,11 @@ function timerType(timer) { if (timer.immediate) { return "Immediate"; - } else if (typeof timer.interval !== "undefined") { + } + if (timer.interval !== undefined) { return "Interval"; - } else { - return "Timeout"; } + return "Timeout"; } function clearTimer(clock, timerId, ttype) { @@ -984,8 +1049,9 @@ if (timerType(timer) === ttype) { delete clock.timers[timerId]; } else { - throw new Error("Cannot clear timer: timer created with set" + ttype + "() but cleared with clear" + timerType(timer) + "()"); - } + throw new Error("Cannot clear timer: timer created with set" + timerType(timer) + + "() but cleared with clear" + ttype + "()"); + } } } @@ -993,16 +1059,20 @@ var method, i, l; + var installedHrTime = "_hrtime"; for (i = 0, l = clock.methods.length; i < l; i++) { method = clock.methods[i]; - - if (target[method].hadOwnProperty) { - target[method] = clock["_" + method]; + if (method === "hrtime" && target.process) { + target.process.hrtime = clock[installedHrTime]; } else { - try { - delete target[method]; - } catch (ignore) {} + if (target[method] && target[method].hadOwnProperty) { + target[method] = clock["_" + method]; + } else { + try { + delete target[method]; + } catch (ignore) { /* eslint empty-block: "off" */ } + } } } @@ -1044,6 +1114,10 @@ Date: Date }; + if (hrtimePresent) { + timers.hrtime = global.process.hrtime; + } + var keys = Object.keys || function (obj) { var ks = [], key; @@ -1059,11 +1133,15 @@ exports.timers = timers; - function createClock(now) { + function createClock(now, loopLimit) { + loopLimit = loopLimit || 1000; + var clock = { now: getEpoch(now), + hrNow: 0, timeouts: {}, - Date: createDate() + Date: createDate(), + loopLimit: loopLimit }; clock.Date.clock = clock; @@ -1113,10 +1191,16 @@ clock.duringTick = true; + function updateHrTime(newNow) { + clock.hrNow += (newNow - clock.now); + } + var firstException; while (timer && tickFrom <= tickTo) { if (clock.timers[timer.id]) { - tickFrom = clock.now = timer.callAt; + updateHrTime(timer.callAt); + tickFrom = timer.callAt; + clock.now = timer.callAt; try { oldNow = clock.now; callTimer(clock, timer); @@ -1136,6 +1220,7 @@ } clock.duringTick = false; + updateHrTime(tickTo); clock.now = tickTo; if (firstException) { @@ -1161,6 +1246,33 @@ } }; + clock.runAll = function runAll() { + var numTimers, i; + for (i = 0; i < clock.loopLimit; i++) { + if (!clock.timers) { + return clock.now; + } + + numTimers = Object.keys(clock.timers).length; + if (numTimers === 0) { + return clock.now; + } + + clock.next(); + } + + throw new Error("Aborting after running " + clock.loopLimit + "timers, assuming an infinite loop!"); + }; + + clock.runToLast = function runToLast() { + var timer = lastTimer(clock); + if (!timer) { + return clock.now; + } + + return clock.tick(timer.callAt); + }; + clock.reset = function reset() { clock.timers = {}; }; @@ -1169,25 +1281,46 @@ // determine time difference var newNow = getEpoch(now); var difference = newNow - clock.now; + var id, timer; // update 'system clock' clock.now = newNow; // update timers and intervals to keep them stable - for (var id in clock.timers) { + for (id in clock.timers) { if (clock.timers.hasOwnProperty(id)) { - var timer = clock.timers[id]; + timer = clock.timers[id]; timer.createdAt += difference; timer.callAt += difference; } } }; + if (hrtimePresent) { + clock.hrtime = function (prev) { + if (Array.isArray(prev)) { + var oldSecs = (prev[0] + prev[1] / 1e9); + var newSecs = (clock.hrNow / 1000); + var difference = (newSecs - oldSecs); + var secs = fixedFloor(difference); + var nanosecs = fixedModulo(difference * 1e9, 1e9); + return [ + secs, + nanosecs + ]; + } + return [ + fixedFloor(clock.hrNow / 1000), + fixedModulo(clock.hrNow * 1e6, 1e9) + ]; + }; + } + return clock; } exports.createClock = createClock; - exports.install = function install(target, now, toFake) { + exports.install = function install(target, now, toFake, loopLimit) { var i, l; @@ -1201,7 +1334,7 @@ target = global; } - var clock = createClock(now); + var clock = createClock(now, loopLimit); clock.uninstall = function () { uninstall(clock, target); @@ -1214,7 +1347,13 @@ } for (i = 0, l = clock.methods.length; i < l; i++) { - hijackMethod(target, clock.methods[i], clock); + if (clock.methods[i] === "hrtime") { + if (target.process && typeof target.process.hrtime === "function") { + hijackMethod(target.process, clock.methods[i], clock); + } + } else { + hijackMethod(target, clock.methods[i], clock); + } } return clock; @@ -1371,9 +1510,17 @@ var sinon = (function () { var error, wrappedMethod, i; + function simplePropertyAssignment() { + wrappedMethod = object[property]; + checkWrappedMethod(wrappedMethod); + object[property] = method; + method.displayName = property; + } + // IE 8 does not support hasOwnProperty on the window object and Firefox has a problem // when using hasOwn.call on objects from other frames. - var owned = object.hasOwnProperty ? object.hasOwnProperty(property) : hasOwn.call(object, property); + var owned = (object.hasOwnProperty && object.hasOwnProperty === hasOwn) ? + object.hasOwnProperty(property) : hasOwn.call(object, property); if (hasES5Support) { var methodDesc = (typeof method === "function") ? {value: method} : method; @@ -1403,11 +1550,17 @@ var sinon = (function () { mirrorProperties(methodDesc[types[i]], wrappedMethodDesc[types[i]]); } Object.defineProperty(object, property, methodDesc); + + // catch failing assignment + // this is the converse of the check in `.restore` below + if ( typeof method === "function" && object[property] !== method ) { + // correct any wrongdoings caused by the defineProperty call above, + // such as adding new items (if object was a Storage object) + delete object[property]; + simplePropertyAssignment(); + } } else { - wrappedMethod = object[property]; - checkWrappedMethod(wrappedMethod); - object[property] = method; - method.displayName = property; + simplePropertyAssignment(); } method.displayName = property; @@ -1431,9 +1584,17 @@ var sinon = (function () { Object.defineProperty(object, property, wrappedMethodDesc); } + // this only supports ES5 getter/setter, for ES3.1 and lower + // __lookupSetter__ / __lookupGetter__ should be integrated + if (hasES5Support) { + var checkDesc = sinon.getPropertyDescriptor(object, property); + if (checkDesc.value === method) { + object[property] = wrappedMethod; + } + // Use strict equality comparison to check failures then force a reset // via direct assignment. - if (object[property] === method) { + } else if (object[property] === method) { object[property] = wrappedMethod; } }; @@ -1497,7 +1658,7 @@ var sinon = (function () { } for (prop in a) { - if (a.hasOwnProperty(prop)) { + if (hasOwn.call(a, prop)) { aLength += 1; if (!(prop in b)) { @@ -1511,7 +1672,7 @@ var sinon = (function () { } for (prop in b) { - if (b.hasOwnProperty(prop)) { + if (hasOwn.call(b, prop)) { bLength += 1; } } @@ -2516,11 +2677,15 @@ var sinon = (function () { } if (types) { + // A new descriptor is needed here because we can only wrap functions + // By passing the original descriptor we would end up trying to spy non-function properties + var descriptor = {}; var methodDesc = sinon.getPropertyDescriptor(object, property); + for (var i = 0; i < types.length; i++) { - methodDesc[types[i]] = spy.create(methodDesc[types[i]]); + descriptor[types[i]] = spy.create(methodDesc[types[i]]); } - return sinon.wrapMethod(object, property, methodDesc); + return sinon.wrapMethod(object, property, descriptor); } return sinon.wrapMethod(object, property, spy.create(object[property])); @@ -3340,7 +3505,7 @@ var sinon = (function () { } Object.getOwnPropertyNames(obj).forEach(function (k) { - if (!seen[k]) { + if (seen[k] !== true) { seen[k] = true; var target = typeof Object.getOwnPropertyDescriptor(obj, k).get === "function" ? originalObj : obj; @@ -4376,8 +4541,8 @@ if (typeof sinon === "undefined") { sinon.ProgressEvent = function ProgressEvent(type, progressEventRaw, target) { this.initEvent(type, false, false, target); - this.loaded = progressEventRaw.loaded || null; - this.total = progressEventRaw.total || null; + this.loaded = typeof progressEventRaw.loaded === "number" ? progressEventRaw.loaded : null; + this.total = typeof progressEventRaw.total === "number" ? progressEventRaw.total : null; this.lengthComputable = !!progressEventRaw.total; }; @@ -4803,7 +4968,13 @@ if (typeof sinon === "undefined") { var supportsCustomEvent = typeof CustomEvent !== "undefined"; var supportsFormData = typeof FormData !== "undefined"; var supportsArrayBuffer = typeof ArrayBuffer !== "undefined"; - var supportsBlob = typeof Blob === "function"; + var supportsBlob = (function () { + try { + return !!new Blob(); + } catch (e) { + return false; + } + })(); var sinonXhr = { XMLHttpRequest: global.XMLHttpRequest }; sinonXhr.GlobalXMLHttpRequest = global.XMLHttpRequest; sinonXhr.GlobalActiveXObject = global.ActiveXObject; @@ -4839,10 +5010,11 @@ if (typeof sinon === "undefined") { // and uploadError. function UploadProgress() { this.eventListeners = { - progress: [], - load: [], abort: [], - error: [] + error: [], + load: [], + loadend: [], + progress: [] }; } @@ -4886,7 +5058,7 @@ if (typeof sinon === "undefined") { } var xhr = this; - var events = ["loadstart", "load", "abort", "loadend"]; + var events = ["loadstart", "load", "abort", "error", "loadend"]; function addEventListener(eventName) { xhr.addEventListener(eventName, function (event) { @@ -5209,6 +5381,7 @@ if (typeof sinon === "undefined") { this.readyState = state; var readyStateChangeEvent = new sinon.Event("readystatechange", false, false, this); + var event, progress; if (typeof this.onreadystatechange === "function") { try { @@ -5218,16 +5391,29 @@ if (typeof sinon === "undefined") { } } - switch (this.readyState) { - case FakeXMLHttpRequest.DONE: - if (supportsProgress) { - this.upload.dispatchEvent(new sinon.ProgressEvent("progress", {loaded: 100, total: 100})); - this.dispatchEvent(new sinon.ProgressEvent("progress", {loaded: 100, total: 100})); - } - this.upload.dispatchEvent(new sinon.Event("load", false, false, this)); - this.dispatchEvent(new sinon.Event("load", false, false, this)); - this.dispatchEvent(new sinon.Event("loadend", false, false, this)); - break; + if (this.readyState === FakeXMLHttpRequest.DONE) { + // ensure loaded and total are numbers + progress = { + loaded: this.progress || 0, + total: this.progress || 0 + }; + + if (this.status === 0) { + event = this.aborted ? "abort" : "error"; + } + else { + event = "load"; + } + + if (supportsProgress) { + this.upload.dispatchEvent(new sinon.ProgressEvent("progress", progress, this)); + this.upload.dispatchEvent(new sinon.ProgressEvent(event, progress, this)); + this.upload.dispatchEvent(new sinon.ProgressEvent("loadend", progress, this)); + } + + this.dispatchEvent(new sinon.ProgressEvent("progress", progress, this)); + this.dispatchEvent(new sinon.ProgressEvent(event, progress, this)); + this.dispatchEvent(new sinon.ProgressEvent("loadend", progress, this)); } this.dispatchEvent(readyStateChangeEvent); @@ -5306,14 +5492,15 @@ if (typeof sinon === "undefined") { } this.readyState = FakeXMLHttpRequest.UNSENT; + }, - this.dispatchEvent(new sinon.Event("abort", false, false, this)); - - this.upload.dispatchEvent(new sinon.Event("abort", false, false, this)); + error: function error() { + clearResponse(this); + this.errorFlag = true; + this.requestHeaders = {}; + this.responseHeaders = {}; - if (typeof this.onerror === "function") { - this.onerror(); - } + this.readyStateChange(FakeXMLHttpRequest.DONE); }, getResponseHeader: function getResponseHeader(header) { @@ -5379,6 +5566,7 @@ if (typeof sinon === "undefined") { } else if (this.responseType === "" && isXmlContentType(contentType)) { this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText); } + this.progress = body.length; this.readyStateChange(FakeXMLHttpRequest.DONE); }, @@ -5925,6 +6113,10 @@ if (typeof sinon === "undefined") { }, restore: function () { + if (arguments.length) { + throw new Error("sandbox.restore() does not take any parameters. Perhaps you meant stub.restore()"); + } + sinon.collection.restore.apply(this, arguments); this.restoreContext(); }, @@ -6050,13 +6242,11 @@ if (typeof sinon === "undefined") { exception = e; } - if (typeof oldDone !== "function") { - if (typeof exception !== "undefined") { - sandbox.restore(); - throw exception; - } else { - sandbox.verifyAndRestore(); - } + if (typeof exception !== "undefined") { + sandbox.restore(); + throw exception; + } else if (typeof oldDone !== "function") { + sandbox.verifyAndRestore(); } return result; @@ -6253,6 +6443,24 @@ if (typeof sinon === "undefined") { } } + function verifyIsValidAssertion(assertionMethod, assertionArgs) { + switch (assertionMethod) { + case "notCalled": + case "called": + case "calledOnce": + case "calledTwice": + case "calledThrice": + if (assertionArgs.length !== 0) { + assert.fail(assertionMethod + + " takes 1 argument but was called with " + + (assertionArgs.length + 1) + " arguments"); + } + break; + default: + break; + } + } + function failAssertion(object, msg) { object = object || global; var failMethod = object.fail || assert.fail; @@ -6269,6 +6477,8 @@ if (typeof sinon === "undefined") { verifyIsStub(fake); var args = slice.call(arguments, 1); + verifyIsValidAssertion(name, args); + var failed = false; if (typeof method === "function") { diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 3b511c27cf..6c8923d551 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -9,14 +9,7 @@ return [ 'test.sinonjs' => [ 'scripts' => [ 'tests/qunit/suites/resources/test.sinonjs/index.js', - 'resources/lib/sinonjs/sinon-1.17.3.js', - // We want tests to work in IE, but can't include this as it - // will break the placeholders in Sinon because the hack it uses - // to hijack IE globals relies on running in the global scope - // and in ResourceLoader this won't be running in the global scope. - // Including it results (among other things) in sandboxed timers - // being broken due to Date inheritance being undefined. - // 'resources/lib/sinonjs/sinon-ie-1.15.4.js', + 'resources/lib/sinonjs/sinon.js', ], 'targets' => [ 'desktop', 'mobile' ], ], -- 2.20.1