From f125f572f836cffec1005ae73ab3a40779776bc3 Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Thu, 21 Feb 2013 18:53:45 -0800 Subject: [PATCH 1/7] First pass at transport rewrite. --- src/js/transport.js | 89 +++++++++++++++++++++++---------------------- src/js/typeahead.js | 66 +++++++++++++++------------------ 2 files changed, 75 insertions(+), 80 deletions(-) diff --git a/src/js/transport.js b/src/js/transport.js index 85306818..e3be7cc3 100644 --- a/src/js/transport.js +++ b/src/js/transport.js @@ -5,46 +5,40 @@ */ var Transport = (function() { + var concurrentConnections = 0, + maxConcurrentConnections; function Transport(o) { - var rateLimitFn; - utils.bindAll(this); o = o || {}; - rateLimitFn = (/^throttle$/i).test(o.rateLimitFn) ? - utils.throttle : utils.debounce; + maxConcurrentConnections = utils.isNumber(o.maxConcurrentConnections) ? + o.maxConcurrentConnections : maxConcurrentConnections || 6; - this.wait = o.wait || 300; this.wildcard = o.wildcard || '%QUERY'; - this.maxConcurrentRequests = o.maxConcurrentRequests || 6; - this.concurrentRequests = 0; - this.onDeckRequestArgs = null; + this.ajaxSettings = utils.mixin({}, o.ajax, { + // needs to be true to jqXHR methods (done, always) + // also you're out of your mind if you want to make a sync request + async: true, + beforeSend: function() { + incrementConcurrentConnections(); + + if (o.ajax.beforeSend) { + return o.ajax.beforeSend.apply(this, arguments); + } + } + }); this.cache = new RequestCache(); - this.get = rateLimitFn(this.get, this.wait); + this.get = (/^throttle$/i.test(o.rateLimitFn) ? + utils.throttle : utils.debounce)(this.get, o.wait || 300); } utils.mixin(Transport.prototype, { - // private methods - // --------------- - - _incrementConcurrentRequests: function() { - this.concurrentRequests++; - }, - - _decrementConcurrentRequests: function() { - this.concurrentRequests--; - }, - - _belowConcurrentRequestsThreshold: function() { - return this.concurrentRequests < this.maxConcurrentRequests; - }, - // public methods // -------------- @@ -57,25 +51,19 @@ var Transport = (function() { cb && cb(resp); } - else if (this._belowConcurrentRequestsThreshold()) { - $.ajax({ - url: url, - type: 'GET', - dataType: 'json', - beforeSend: function() { - that._incrementConcurrentRequests(); - }, - success: function(resp) { - cb && cb(resp); - that.cache.set(url, resp); - }, - complete: function() { - that._decrementConcurrentRequests(); - - if (that.onDeckRequestArgs) { - that.get.apply(that, that.onDeckRequestArgs); - that.onDeckRequestArgs = null; - } + else if (belowConcurrentConnectionsThreshold()) { + $.ajax(this.ajaxSettings) + .done(function(resp) { + cb && cb(resp); + that.cache.set(url, resp); + }) + .always(function() { + decrementConcurrentConnections(); + + // ensures request is always made for the latest query + if (that.onDeckRequestArgs) { + that.get.apply(that, that.onDeckRequestArgs); + that.onDeckRequestArgs = null; } }); } @@ -87,4 +75,19 @@ var Transport = (function() { }); return Transport; + + // static methods + // -------------- + + function incrementConcurrentConnections() { + concurrentConnections++; + } + + function decrementConcurrentConnections() { + concurrentConnections--; + } + + function belowConcurrentConnectionsThreshold() { + return concurrentConnections < maxConcurrentConnections; + } })(); diff --git a/src/js/typeahead.js b/src/js/typeahead.js index 3dbacf06..c63d3341 100644 --- a/src/js/typeahead.js +++ b/src/js/typeahead.js @@ -5,13 +5,7 @@ */ (function() { - var initializedDatasets = {}, - transportOptions = {}, - transport, - methods; - - jQuery.fn.typeahead = typeahead; - typeahead.configureTransport = configureTransport; + var initializedDatasets = {}, methods; methods = { initialize: function(datasetDefs) { @@ -23,44 +17,46 @@ throw new Error('no datasets provided'); } - delete typeahead.configureTransport; - transport = transport || new Transport(transportOptions); + utils.each(datasetDefs, function(i, o) { + var transport, dataset; - utils.each(datasetDefs, function(i, datasetDef) { - var dataset, - name = datasetDef.name = datasetDef.name || utils.getUniqueId(); + o.name = o.name || utils.getUniqueId(); // dataset by this name has already been intialized, used it - if (initializedDatasets[name]) { - dataset = initializedDatasets[name]; + if (initializedDatasets[o.name]) { + dataset = initializedDatasets[o.name]; } else { - datasetDef.limit = datasetDef.limit || 5; - datasetDef.template = datasetDef.template; - datasetDef.engine = datasetDef.engine; - - if (datasetDef.template && !datasetDef.engine) { - throw new Error('no template engine specified for ' + name); + if (o.template && !o.engine) { + throw new Error('no template engine specified for ' + o.name); } + transport = new Transport({ + ajax: o.ajax, + wildcard: o.wildcard, + rateLimitFn: o.rateLimitFn, + maxConcurrentConnections: o.maxConcurrentConnections + }); + dataset = initializedDatasets[name] = new Dataset({ - name: datasetDef.name, - limit: datasetDef.limit, - local: datasetDef.local, - prefetch: datasetDef.prefetch, - remote: datasetDef.remote, - matcher: datasetDef.matcher, - ranker: datasetDef.ranker, + name: o.name, + limit: o.limit, + local: o.local, + prefetch: o.prefetch, + remote: o.remote, + matcher: o.matcher, + ranker: o.ranker, transport: transport }); } + // faux dataset used by TypeaheadView instances datasets[name] = { - name: datasetDef.name, - limit: datasetDef.limit, - template: datasetDef.template, - engine: datasetDef.engine, + name: o.name, + limit: o.limit, + template: o.template, + engine: o.engine, getSuggestions: dataset.getSuggestions }; }); @@ -73,7 +69,7 @@ } }; - function typeahead(method) { + jQuery.fn.typeahead = function(method) { if (methods[method]) { methods[method].apply(this, [].slice.call(arguments, 1)); } @@ -81,9 +77,5 @@ else { methods.initialize.apply(this, arguments); } - } - - function configureTransport(o) { - transportOptions = o; - } + }; })(); From ceff35805a960bd28455e27346b6ac93f8ed2d98 Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Fri, 22 Feb 2013 09:34:00 -0800 Subject: [PATCH 2/7] Have transports share a request cache. --- src/js/transport.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/js/transport.js b/src/js/transport.js index e3be7cc3..0d4a30ec 100644 --- a/src/js/transport.js +++ b/src/js/transport.js @@ -6,7 +6,8 @@ var Transport = (function() { var concurrentConnections = 0, - maxConcurrentConnections; + maxConcurrentConnections, + requestCache; function Transport(o) { utils.bindAll(this); @@ -31,7 +32,7 @@ var Transport = (function() { } }); - this.cache = new RequestCache(); + requestCache = requestCache || new RequestCache(); this.get = (/^throttle$/i.test(o.rateLimitFn) ? utils.throttle : utils.debounce)(this.get, o.wait || 300); @@ -47,7 +48,7 @@ var Transport = (function() { url = url.replace(this.wildcard, encodeURIComponent(query || '')); - if (resp = this.cache.get(url)) { + if (resp = requestCache.get(url)) { cb && cb(resp); } @@ -55,7 +56,7 @@ var Transport = (function() { $.ajax(this.ajaxSettings) .done(function(resp) { cb && cb(resp); - that.cache.set(url, resp); + requestCache.set(url, resp); }) .always(function() { decrementConcurrentConnections(); From fd397abe7ae9831d27169ec7e66996e0c40b3818 Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Mon, 25 Feb 2013 21:02:14 -0800 Subject: [PATCH 3/7] Add support for new API. --- src/js/dataset.js | 50 ++++++---- src/js/transport.js | 101 +++++++++++--------- src/js/typeahead.js | 59 +++--------- test/dataset_spec.js | 8 +- test/transport_spec.js | 212 +++++++++++++++++++---------------------- 5 files changed, 199 insertions(+), 231 deletions(-) diff --git a/src/js/dataset.js b/src/js/dataset.js index e97d0196..dc2dfcc5 100644 --- a/src/js/dataset.js +++ b/src/js/dataset.js @@ -9,18 +9,18 @@ var Dataset = (function() { function Dataset(o) { utils.bindAll(this); - this.storage = new PersistentStorage(o.name); - this.adjacencyList = {}; - this.itemHash = {}; + if (o.template && !o.engine) { + throw new Error('no template engine specified'); + } + + if(!o.local && !o.prefetch && !o.remote) { + throw new Error('one of local, prefetch, or remote is requried'); + } this.name = o.name; - this.resetDataOnProtocolSwitch = o.resetDataOnProtocolSwitch || false; - this.queryUrl = o.remote; - this.transport = o.transport; - this.limit = o.limit || 10; - this._customMatcher = o.matcher || null; - this._customRanker = o.ranker || null; - this._ttl_ms = o.ttl_ms || 3 * 24 * 60 * 60 * 1000; // 3 days; + this.limit = o.limit || 5; + this.template = o.template; + this.engine = o.engine; this.keys = { version: 'version', @@ -29,6 +29,12 @@ var Dataset = (function() { adjacencyList: 'adjacencyList' }; + this.itemHash = {}; + this.adjacencyList = {}; + this.storage = new PersistentStorage(o.name); + + this.transport = o.remote ? new Transport(o.remote) : null; + o.local && this._processLocalData(o.local); o.prefetch && this._loadPrefetchData(o.prefetch); } @@ -42,12 +48,13 @@ var Dataset = (function() { data && this._mergeProcessedData(this._processData(data)); }, - _loadPrefetchData: function(url) { + _loadPrefetchData: function(o) { var that = this, + ttl = o.ttl || 3 * 24 * 60 * 60 * 1000, // 3 days + version = this.storage.get(this.keys.version), + protocol = this.storage.get(this.keys.protocol), itemHash = this.storage.get(this.keys.itemHash), adjacencyList = this.storage.get(this.keys.adjacencyList), - protocol = this.storage.get(this.keys.protocol), - version = this.storage.get(this.keys.version), isExpired = version !== VERSION || protocol !== utils.getProtocol(); // data was available in local storage, use it @@ -59,20 +66,21 @@ var Dataset = (function() { } else { - $.getJSON(url).done(processPrefetchData); + $.getJSON(o.url).done(processPrefetchData); } function processPrefetchData(data) { - var processedData = that._processData(data), + var filteredData = o.filter ? o.filter(data) : data, + processedData = that._processData(data), itemHash = processedData.itemHash, adjacencyList = processedData.adjacencyList; // store process data in local storage // this saves us from processing the data on every page load - that.storage.set(that.keys.itemHash, itemHash, that._ttl_ms); - that.storage.set(that.keys.adjacencyList, adjacencyList, that._ttl_ms); - that.storage.set(that.keys.version, VERSION, that._ttl_ms); - that.storage.set(that.keys.protocol, utils.getProtocol(), that._ttl_ms); + that.storage.set(that.keys.itemHash, itemHash, ttl); + that.storage.set(that.keys.adjacencyList, adjacencyList, ttl); + that.storage.set(that.keys.version, VERSION, ttl); + that.storage.set(that.keys.protocol, utils.getProtocol(), ttl); that._mergeProcessedData(processedData); } @@ -255,8 +263,8 @@ var Dataset = (function() { var matchedItems = utils.filter(potentiallyMatchingItems, this._matcher(terms)); matchedItems.sort(this._ranker); callback && callback(matchedItems); - if (matchedItems.length < this.limit && this.queryUrl) { - this.transport.get(this.queryUrl, query, this._processRemoteSuggestions(callback, matchedItems)); + if (matchedItems.length < this.limit && this.transport) { + this.transport.get(query, this._processRemoteSuggestions(callback, matchedItems)); } } }); diff --git a/src/js/transport.js b/src/js/transport.js index 1f1c81bd..6fd30ad0 100644 --- a/src/js/transport.js +++ b/src/js/transport.js @@ -5,37 +5,36 @@ */ var Transport = (function() { - var concurrentConnections = 0, - maxConcurrentConnections, - requestCache; + var pendingRequests = 0, maxParallelRequests, requestCache; function Transport(o) { utils.bindAll(this); - o = o || {}; - - maxConcurrentConnections = utils.isNumber(o.maxConcurrentConnections) ? - o.maxConcurrentConnections : maxConcurrentConnections || 6; + if (!utils.isObject(o) || !o.url) { + throw new Error('invalid settings for remote'); + } - this.wildcard = o.wildcard || '%QUERY'; + requestCache = requestCache || new RequestCache(); - this.ajaxSettings = utils.mixin({}, o.ajax, { - // needs to be true to jqXHR methods (done, always) - // also you're out of your mind if you want to make a sync request - async: true, - beforeSend: function() { - incrementConcurrentConnections(); + // shared between all instances, last instance to set it wins + maxParallelRequests = utils.isNumber(o.maxParallelRequests) ? + o.maxParallelRequests : maxParallelRequests || 6; - if (o.ajax.beforeSend) { - return o.ajax.beforeSend.apply(this, arguments); - } - } - }); + this.url = o.url; + this.wildcard = o.wildcard || '%QUERY'; + this.filter = o.filter; + this.replace = o.replace; - requestCache = requestCache || new RequestCache(); + this.ajaxSettings = { + type: 'get', + cache: o.cache, + timeout: o.timeout, + dataType: o.dataType || 'json', + beforeSend: o.beforeSend + }; this.get = (/^throttle$/i.test(o.rateLimitFn) ? - utils.throttle : utils.debounce)(this.get, o.wait || 300); + utils.throttle : utils.debounce)(this.get, o.rateLimitWait || 300); } utils.mixin(Transport.prototype, { @@ -43,35 +42,47 @@ var Transport = (function() { // public methods // -------------- - get: function(url, query, cb) { - var that = this, resp; + get: function(query, cb) { + var that = this, + encodedQuery = encodeURIComponent(query || ''), + url, + resp; - url = url.replace(this.wildcard, encodeURIComponent(query || '')); + url = this.replace ? + this.replace(this.url, encodedQuery) : + this.url.replace(this.wildcard, encodedQuery); if (resp = requestCache.get(url)) { cb && cb(resp); } - else if (belowConcurrentConnectionsThreshold()) { - $.ajax(this.ajaxSettings) - .done(function(resp) { - cb && cb(resp); - requestCache.set(url, resp); - }) - .always(function() { - decrementConcurrentConnections(); - - // ensures request is always made for the latest query - if (that.onDeckRequestArgs) { - that.get.apply(that, that.onDeckRequestArgs); - that.onDeckRequestArgs = null; - } - }); + else if (belowPendingRequestsThreshold()) { + incrementPendingRequests(); + $.ajax(url, this.ajaxSettings).done(done).always(always); } else { this.onDeckRequestArgs = [].slice.call(arguments, 0); } + + // success callback + function done(resp) { + resp = that.filter ? that.filter(resp) : resp; + + cb && cb(resp); + requestCache.set(url, resp); + } + + // comlete callback + function always() { + decrementPendingRequests(); + + // ensures request is always made for the latest query + if (that.onDeckRequestArgs) { + that.get.apply(that, that.onDeckRequestArgs); + that.onDeckRequestArgs = null; + } + } } }); @@ -80,15 +91,15 @@ var Transport = (function() { // static methods // -------------- - function incrementConcurrentConnections() { - concurrentConnections++; + function incrementPendingRequests() { + pendingRequests++; } - function decrementConcurrentConnections() { - concurrentConnections--; + function decrementPendingRequests() { + pendingRequests--; } - function belowConcurrentConnectionsThreshold() { - return concurrentConnections < maxConcurrentConnections; + function belowPendingRequestsThreshold() { + return pendingRequests < maxParallelRequests; } })(); diff --git a/src/js/typeahead.js b/src/js/typeahead.js index a2a7a683..821fe78d 100644 --- a/src/js/typeahead.js +++ b/src/js/typeahead.js @@ -5,7 +5,7 @@ */ (function() { - var initializedDatasets = {}, methods; + var datasetCache = {}, methods; methods = { initialize: function(datasetDefs) { @@ -13,59 +13,30 @@ datasetDefs = utils.isArray(datasetDefs) ? datasetDefs : [datasetDefs]; + if (this.length === 0) { + throw new Error('typeahead initialized without DOM element'); + } + if (datasetDefs.length === 0) { throw new Error('no datasets provided'); } utils.each(datasetDefs, function(i, o) { - var transport, dataset; - o.name = o.name || utils.getUniqueId(); - // dataset by this name has already been intialized, used it - if (initializedDatasets[o.name]) { - dataset = initializedDatasets[o.name]; - } - - else { - if (o.template && !o.engine) { - throw new Error('no template engine specified for ' + o.name); - } - - transport = new Transport({ - ajax: o.ajax, - wildcard: o.wildcard, - rateLimitFn: o.rateLimitFn, - maxConcurrentConnections: o.maxConcurrentConnections - }); - - dataset = initializedDatasets[name] = new Dataset({ - name: o.name, - limit: o.limit, - local: o.local, - prefetch: o.prefetch, - ttl_ms: o.ttl_ms, // temporary – will be removed in future - remote: o.remote, - matcher: o.matcher, - ranker: o.ranker, - transport: transport - }); - } - - // faux dataset used by TypeaheadView instances - datasets[name] = { - name: o.name, - limit: o.limit, - template: o.template, - engine: o.engine, - getSuggestions: dataset.getSuggestions - }; + datasets[o.name] = datasetCache[o.name] ? + datasetCache[o.name] : + datasetCache[o.name] = new Dataset(o); }); return this.each(function() { - $(this).data({ - typeahead: new TypeaheadView({ input: this, datasets: datasets }) - }); + var $input = $(this), + typeaheadView = new TypeaheadView({ + input: $input, + datasets: datasets + }); + + $input.data('ttView', typeaheadView); }); } }; diff --git a/test/dataset_spec.js b/test/dataset_spec.js index e65977f3..b516d379 100644 --- a/test/dataset_spec.js +++ b/test/dataset_spec.js @@ -170,9 +170,7 @@ describe('Dataset', function() { }); it('network requests are not triggered with enough local results', function() { - this.dataset.queryUrl = '/remote?q=%QUERY'; - this.dataset.transport = new Transport({debounce:utils.debounce}); - spyOn(this.dataset.transport, 'get'); + spyOn(this.dataset.transport = { get: $.noop }, 'get'); this.dataset.limit = 1; this.dataset.getSuggestions('c', function(items) { @@ -183,7 +181,7 @@ describe('Dataset', function() { ]); }); - expect(this.dataset.transport.get.callCount).toBe(0); + expect(this.dataset.transport.get).not.toHaveBeenCalled(); this.dataset.limit = 100; this.dataset.getSuggestions('c', function(items) { @@ -194,7 +192,7 @@ describe('Dataset', function() { ]); }); - expect(this.dataset.transport.get.callCount).toBe(1); + expect(this.dataset.transport.get).toHaveBeenCalled(); }); it('matches', function() { diff --git a/test/transport_spec.js b/test/transport_spec.js index 2a7e3dc9..c7e9e1d3 100644 --- a/test/transport_spec.js +++ b/test/transport_spec.js @@ -1,198 +1,178 @@ describe('Transport', function() { - var successResp = { prop: 'val' }, - ajaxMocks = { - timeout: function(o) { - o.beforeSend && o.beforeSend(); - }, - success: function(o) { - o.beforeSend && o.beforeSend(); - - setTimeout(function() { - o.success && o.success(successResp); - o.complete && o.complete(); - }, 50); - }, - error: function(o) { - o.beforeSend && o.beforeSend(); - - setTimeout(function() { - o.error && o.error(); - o.complete && o.complete(); - }, 50); - } - }, - _debounce; + var successData = { prop: 'val' }, + successResp = { status: 200, responseText: JSON.stringify(successData) }, + errorResp = { status: 500 }, + _debounce, + _RequestCache; beforeEach(function() { - spyOn($, 'ajax'); + jasmine.Ajax.useMock(); + + _RequestCache = RequestCache; + RequestCache = MockRequestCache; _debounce = utils.debounce; utils.debounce = function(fn) { return fn; }; this.transport = new Transport({ - wildcard: '%QUERY', + url: 'http://example.com?q=$$', + wildcard: '$$', debounce: true, - maxConcurrentRequests: 3 + maxParallelRequests: 3 }); + + this.requestCache = MockRequestCache.instance; + spyOn(this.requestCache, 'get'); + spyOn(this.requestCache, 'set'); }); afterEach(function() { utils.debounce = _debounce; + RequestCache = _RequestCache; + + // run twice to flush out on-deck requests + for (var i = 0; i < 2; i ++) { + ajaxRequests.forEach(respond); + } + + clearAjaxRequests(); + + function respond(req) { req.response(successResp); } }); describe('#get', function() { describe('when request is available in cache', function() { beforeEach(function() { - spyOn(this.transport.cache, 'get').andReturn(successResp); + this.spy = jasmine.createSpy(); + this.requestCache.get.andReturn(successData); + + this.transport.get('query', this.spy); + this.request = mostRecentAjaxRequest(); }); it('should not call $.ajax', function() { - this.transport.get('http://example.com', 'query'); - - expect($.ajax).not.toHaveBeenCalled(); + expect(this.request).toBeNull(); }); it('should invoke callback with response from cache', function() { - var spy = jasmine.createSpy(); - - this.transport.get('http://example.com', 'query', spy); - - waitsFor(function() { return spy.callCount; }); - runs(function() { expect(spy).toHaveBeenCalledWith(successResp); }); + expect(this.spy).toHaveBeenCalledWith(successData); }); }); - describe('when below concurrent request threshold', function() { - beforeEach(function() { - $.ajax.andCallFake(ajaxMocks.timeout); - }); - + describe('when below pending requests threshold', function() { it('should make remote request', function() { - this.transport.get('http://example.com', 'query'); + this.transport.get('has space'); + this.request = mostRecentAjaxRequest(); - waitsFor(function() { return $.ajax.callCount === 1; }); + expect(this.request).not.toBeNull(); }); it('should replace wildcard in url with encoded query', function() { - var args; + this.transport.get('has space'); + this.request = mostRecentAjaxRequest(); - this.transport.get('http://example.com?q=%QUERY', 'has space'); - args = $.ajax.mostRecentCall.args; - - expect(args[0].url).toEqual('http://example.com?q=has%20space'); - }); + expect(this.request.url).toEqual('http://example.com?q=has%20space'); - it('should increment the concurrent request count', function() { - this.transport.get('http://example.com', 'query'); + this.transport.replace = function(url, query) { return url + query; }; + this.transport.get('has space'); + this.request = mostRecentAjaxRequest(); - expect(this.transport.concurrentRequests).toEqual(1); + expect(this.request.url).toEqual('http://example.com?q=$$has%20space'); }); }); describe('when at concurrent request threshold', function() { beforeEach(function() { - $.ajax.andCallFake(ajaxMocks.timeout); - this.transport.concurrentRequests = - this.transport.maxConcurrentRequests + 1; + this.goodRequests = []; + + for (var i = 0; i < 3; i++) { + this.transport.get('good'); + this.goodRequests.push(mostRecentAjaxRequest()); + } + + this.transport.get('bad', $.noop); }); it('should not call $.ajax', function() { - this.transport.get('http://example.com', 'query'); - - expect($.ajax).not.toHaveBeenCalled(); + expect(ajaxRequests.length).toBe(3); }); it('should set args for the on-deck request', function() { - var cb = function() {}; - - this.transport.get('http://example.com', 'query', cb); - - expect(this.transport.onDeckRequestArgs) - .toEqual(['http://example.com', 'query', cb]); + expect(this.transport.onDeckRequestArgs).toEqual(['bad', $.noop]); }); }); describe('when request succeeds', function() { beforeEach(function() { - $.ajax.andCallFake(ajaxMocks.success); - }); - - it('should invoke callback with json response', function() { - var spy = jasmine.createSpy(); + this.spy = jasmine.createSpy(); - this.transport.get('http://example.com', 'query', spy); + this.transport.filter = jasmine.createSpy().andReturn({ prop: 'val' }); - waitsFor(function() { return spy.callCount; }); - runs(function() { expect(spy).toHaveBeenCalledWith(successResp); }); + this.transport.get('has space', this.spy); + this.request = mostRecentAjaxRequest(); + this.request.response(successResp); }); - it('should decrement the request count', function() { - this.transport.get('http://example.com', 'query'); + it('should invoke callback with json response', function() { + var spy = jasmine.createSpy(); - expect(this.transport.concurrentRequests).toEqual(1); - waitsFor(function() { - return this.transport.concurrentRequests === 0; - }); + expect(this.spy).toHaveBeenCalledWith(successData); }); it('should add response to the cache', function() { - spyOn(this.transport.cache, 'set'); - - this.transport.get('http://example.com', 'query'); - - waitsFor(function() { - return this.transport.cache.set.callCount; - }); + expect(this.requestCache.set) + .toHaveBeenCalledWith('http://example.com?q=has%20space', successData); + }); - runs(function() { - expect(this.transport.cache.set) - .toHaveBeenCalledWith('http://example.com', successResp); - }); + it('should call filter', function() { + expect(this.transport.filter).toHaveBeenCalledWith(successData); }); }); describe('when request fails', function() { beforeEach(function() { - $.ajax.andCallFake(ajaxMocks.error); + this.spy = jasmine.createSpy(); + + this.transport.get('has space', this.spy); + this.request = mostRecentAjaxRequest(); + this.request.response(errorResp); }); - it('should decrement the request count', function() { - this.transport.get('http://example.com', 'query'); + it('should not invoke callback', function() { + expect(this.spy).not.toHaveBeenCalled(); + }); - expect(this.transport.concurrentRequests).toEqual(1); - waitsFor(function() { - return this.transport.concurrentRequests === 0; - }); + it('should not add response to the cache', function() { + expect(this.requestCache.set).not.toHaveBeenCalled(); }); }); describe('when request count drops below threshold', function() { - beforeEach(function() { - $.ajax.andCallFake(ajaxMocks.success); - }); - it('should call #get with on-deck request args', function() { - var spy = jasmine.createSpy(), - i = this.transport.maxConcurrentRequests; + var requests = []; - while(i--) { this.transport.get('http://example.com', 'query', spy); } - - // above the threshold, should be delayed - this.transport.get('http://example.com', 'query', spy); - - spyOn(this.transport, 'get'); + for (var i = 0; i < 3; i++) { + this.transport.get('good'); + requests.push(mostRecentAjaxRequest()); + } - waitsFor(function() { - return spy.callCount === this.transport.maxConcurrentRequests && - this.transport.get.callCount === 1; - }); + this.transport.get('bad'); - runs(function() { - expect(this.transport.get) - .toHaveBeenCalledWith('http://example.com', 'query', spy); - }); + expect(ajaxRequests.length).toBe(3); + requests[0].response(successResp); + expect(ajaxRequests.length).toBe(4); + expect(mostRecentAjaxRequest().url).toBe('http://example.com?q=bad'); }); }); }); + + // helper functions + // ---------------- + + function MockRequestCache() { + this.get = $.noop; + this.set = $.noop; + MockRequestCache.instance = this; + } }); From bdb56e3776c52efd164c03b837486df29834cdda Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Mon, 25 Feb 2013 22:08:32 -0800 Subject: [PATCH 4/7] Support original API. --- src/js/dataset.js | 12 +++++++----- src/js/transport.js | 4 +--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/js/dataset.js b/src/js/dataset.js index 6fb07f17..d9ca1a57 100644 --- a/src/js/dataset.js +++ b/src/js/dataset.js @@ -49,13 +49,15 @@ var Dataset = (function() { _loadPrefetchData: function(o) { var that = this, - ttl = o.ttl || 3 * 24 * 60 * 60 * 1000, // 3 days version = this.storage.get(this.keys.version), protocol = this.storage.get(this.keys.protocol), itemHash = this.storage.get(this.keys.itemHash), adjacencyList = this.storage.get(this.keys.adjacencyList), isExpired = version !== VERSION || protocol !== utils.getProtocol(); + o = utils.isString(o) ? { url: o } : o; + o.ttl = o.ttl || 3 * 24 * 60 * 60 * 1000; // 3 days + // data was available in local storage, use it if (itemHash && adjacencyList && !isExpired) { this._mergeProcessedData({ @@ -76,10 +78,10 @@ var Dataset = (function() { // store process data in local storage // this saves us from processing the data on every page load - that.storage.set(that.keys.itemHash, itemHash, ttl); - that.storage.set(that.keys.adjacencyList, adjacencyList, ttl); - that.storage.set(that.keys.version, VERSION, ttl); - that.storage.set(that.keys.protocol, utils.getProtocol(), ttl); + that.storage.set(that.keys.itemHash, itemHash, o.ttl); + that.storage.set(that.keys.adjacencyList, adjacencyList, o.ttl); + that.storage.set(that.keys.version, VERSION, o.ttl); + that.storage.set(that.keys.protocol, utils.getProtocol(), o.ttl); that._mergeProcessedData(processedData); } diff --git a/src/js/transport.js b/src/js/transport.js index 6fd30ad0..8452cc3a 100644 --- a/src/js/transport.js +++ b/src/js/transport.js @@ -10,9 +10,7 @@ var Transport = (function() { function Transport(o) { utils.bindAll(this); - if (!utils.isObject(o) || !o.url) { - throw new Error('invalid settings for remote'); - } + o = utils.isString(o) ? { url: o } : o; requestCache = requestCache || new RequestCache(); From 7632109359f2c22cfa831c0097c9de9e6f1e40ed Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Tue, 26 Feb 2013 18:48:58 -0800 Subject: [PATCH 5/7] Add request cache mock for tests. --- test/helpers/mock_request_cache.js | 27 +++++++++++++++++++++++++++ test/transport_spec.js | 10 +++------- 2 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 test/helpers/mock_request_cache.js diff --git a/test/helpers/mock_request_cache.js b/test/helpers/mock_request_cache.js new file mode 100644 index 00000000..d83a7240 --- /dev/null +++ b/test/helpers/mock_request_cache.js @@ -0,0 +1,27 @@ +(function() { + var _RequestCache; + + jasmine.RequestCache = { + useMock: function() { + var spec = jasmine.getEnv().currentSpec; + + spec.after(jasmine.RequestCache.uninstallMock); + jasmine.RequestCache.installMock(); + }, + + installMock: function() { + _RequestCache = RequestCache; + RequestCache = MockRequestCache; + }, + + uninstallMock: function() { + RequestCache = _RequestCache; + } + }; + + function MockRequestCache() { + this.get = $.noop; + this.set = $.noop; + MockRequestCache.instance = this; + } +})(); diff --git a/test/transport_spec.js b/test/transport_spec.js index c7e9e1d3..e641836c 100644 --- a/test/transport_spec.js +++ b/test/transport_spec.js @@ -2,14 +2,11 @@ describe('Transport', function() { var successData = { prop: 'val' }, successResp = { status: 200, responseText: JSON.stringify(successData) }, errorResp = { status: 500 }, - _debounce, - _RequestCache; + _debounce; beforeEach(function() { jasmine.Ajax.useMock(); - - _RequestCache = RequestCache; - RequestCache = MockRequestCache; + jasmine.RequestCache.useMock(); _debounce = utils.debounce; utils.debounce = function(fn) { return fn; }; @@ -21,14 +18,13 @@ describe('Transport', function() { maxParallelRequests: 3 }); - this.requestCache = MockRequestCache.instance; + this.requestCache = RequestCache.instance; spyOn(this.requestCache, 'get'); spyOn(this.requestCache, 'set'); }); afterEach(function() { utils.debounce = _debounce; - RequestCache = _RequestCache; // run twice to flush out on-deck requests for (var i = 0; i < 2; i ++) { From 6674bd1e8815fa39a4600899e0457cff565f1283 Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Tue, 26 Feb 2013 19:20:37 -0800 Subject: [PATCH 6/7] Add tests. --- src/js/dataset.js | 2 +- test/dataset_spec.js | 48 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/js/dataset.js b/src/js/dataset.js index d9ca1a57..9b1dd6d8 100644 --- a/src/js/dataset.js +++ b/src/js/dataset.js @@ -72,7 +72,7 @@ var Dataset = (function() { function processPrefetchData(data) { var filteredData = o.filter ? o.filter(data) : data, - processedData = that._processData(data), + processedData = that._processData(filteredData), itemHash = processedData.itemHash, adjacencyList = processedData.adjacencyList; diff --git a/test/dataset_spec.js b/test/dataset_spec.js index b516d379..1388a663 100644 --- a/test/dataset_spec.js +++ b/test/dataset_spec.js @@ -57,6 +57,54 @@ describe('Dataset', function() { }); describe('when initialized', function() { + describe('without local, prefetch, or remote data', function() { + beforeEach(function() { + this.fn = function() { var dataset = new Dataset({ name: 'local' }); }; + }); + + it('should throw an error', function() { + expect(this.fn).toThrow(); + }); + }); + + describe('with a template but no engine', function() { + beforeEach(function() { + this.fn = function() { var dataset = new Dataset({ template: '%' }); }; + }); + + it('should throw an error', function() { + expect(this.fn).toThrow(); + }); + }); + + describe('with no template', function() { + beforeEach(function() { + this.dataset = new Dataset({ local: fixtureData }); + }); + + it('should compile default template', function() { + expect(this.dataset.template.render({ value: 'boo' })) + .toBe('
  • boo

  • '); + }); + }); + + describe('with a template and engine', function() { + beforeEach(function() { + this.dataset = new Dataset({ + local: fixtureData, + template: '%', + engine: { compile: this.spy = jasmine.createSpy().andReturn('boo') } + }); + }); + + it('should compile the template', function() { + expect(this.spy) + .toHaveBeenCalledWith('
  • %
  • '); + + expect(this.dataset.template).toBe('boo'); + }); + }); + describe('with local data', function () { beforeEach(function() { this.dataset = new Dataset({ name: 'local', local: fixtureData }); From 77656a5226c5d47546d6160ad15600771919011a Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Tue, 26 Feb 2013 23:30:21 -0800 Subject: [PATCH 7/7] Work on test suite. --- src/js/dataset.js | 26 ++-- src/js/typeahead.js | 2 +- test/dataset_spec.js | 190 ++++++++++++++++++----------- test/helpers/mock_components.js | 73 +++++++++++ test/helpers/mock_request_cache.js | 27 ---- test/transport_spec.js | 11 +- 6 files changed, 212 insertions(+), 117 deletions(-) create mode 100644 test/helpers/mock_components.js delete mode 100644 test/helpers/mock_request_cache.js diff --git a/src/js/dataset.js b/src/js/dataset.js index 9b1dd6d8..b3a8e74c 100644 --- a/src/js/dataset.js +++ b/src/js/dataset.js @@ -13,10 +13,6 @@ var Dataset = (function() { throw new Error('no template engine specified'); } - if(!o.local && !o.prefetch && !o.remote) { - throw new Error('one of local, prefetch, or remote is requried'); - } - this.name = o.name; this.limit = o.limit || 5; this.template = compileTemplate(o.template, o.engine); @@ -31,11 +27,6 @@ var Dataset = (function() { this.itemHash = {}; this.adjacencyList = {}; this.storage = new PersistentStorage(o.name); - - this.transport = o.remote ? new Transport(o.remote) : null; - - o.local && this._processLocalData(o.local); - o.prefetch && this._loadPrefetchData(o.prefetch); } utils.mixin(Dataset.prototype, { @@ -44,7 +35,7 @@ var Dataset = (function() { // --------------- _processLocalData: function(data) { - data && this._mergeProcessedData(this._processData(data)); + this._mergeProcessedData(this._processData(data)); }, _loadPrefetchData: function(o) { @@ -257,6 +248,21 @@ var Dataset = (function() { // public methods // --------------- + // the contents of this function are broken out of the constructor + // to help improve the testability of datasets + initialize: function(o) { + if (!o.local && !o.prefetch && !o.remote) { + throw new Error('one of local, prefetch, or remote is requried'); + } + + this.transport = o.remote ? new Transport(o.remote) : null; + + o.local && this._processLocalData(o.local); + o.prefetch && this._loadPrefetchData(o.prefetch); + + return this; + }, + getSuggestions: function(query, callback) { var terms = utils.tokenizeQuery(query); var potentiallyMatchingIds = this._getPotentiallyMatchingIds(terms); diff --git a/src/js/typeahead.js b/src/js/typeahead.js index bd904090..85e17261 100644 --- a/src/js/typeahead.js +++ b/src/js/typeahead.js @@ -26,7 +26,7 @@ return datasetCache[o.name] ? datasetCache[o.name] : - datasetCache[o.name] = new Dataset(o); + datasetCache[o.name] = new Dataset(o).initialize(o); }); return this.each(function() { diff --git a/test/dataset_spec.js b/test/dataset_spec.js index 1388a663..a131fb75 100644 --- a/test/dataset_spec.js +++ b/test/dataset_spec.js @@ -20,14 +20,6 @@ describe('Dataset', function() { getMiss: function() { return null; }, - getMissGenerator: function(key) { - var regex = new RegExp(key); - - return function(k) { - return regex.test(k) ? - mockStorageFns.getMiss(key) : mockStorageFns.getHit(key); - }; - }, getHit: function(key) { if (/itemHash/.test(key)) { return expectedItemHash; @@ -48,28 +40,26 @@ describe('Dataset', function() { }; beforeEach(function() { - localStorage.clear(); - jasmine.Ajax.useMock(); - clearAjaxRequests(); + jasmine.PersistentStorage.useMock(); + jasmine.Transport.useMock(); spyOn(utils, 'getUniqueId').andCallFake(function(name) { return name; }); }); - describe('when initialized', function() { - describe('without local, prefetch, or remote data', function() { - beforeEach(function() { - this.fn = function() { var dataset = new Dataset({ name: 'local' }); }; - }); + afterEach(function() { + clearAjaxRequests(); + }); - it('should throw an error', function() { - expect(this.fn).toThrow(); - }); + describe('#constructor', function() { + it('should initialize persistent storage', function() { + expect(new Dataset({ local: fixtureData }).storage).toBeDefined(); + expect(PersistentStorage).toHaveBeenCalled(); }); - describe('with a template but no engine', function() { + describe('when called with a template but no engine', function() { beforeEach(function() { - this.fn = function() { var dataset = new Dataset({ template: '%' }); }; + this.fn = function() { var d = new Dataset({ template: 't' }); }; }); it('should throw an error', function() { @@ -77,7 +67,7 @@ describe('Dataset', function() { }); }); - describe('with no template', function() { + describe('when called with no template', function() { beforeEach(function() { this.dataset = new Dataset({ local: fixtureData }); }); @@ -88,44 +78,55 @@ describe('Dataset', function() { }); }); - describe('with a template and engine', function() { + describe('when called with a template and engine', function() { beforeEach(function() { this.dataset = new Dataset({ local: fixtureData, - template: '%', + template: 't', engine: { compile: this.spy = jasmine.createSpy().andReturn('boo') } }); }); it('should compile the template', function() { expect(this.spy) - .toHaveBeenCalledWith('
  • %
  • '); + .toHaveBeenCalledWith('
  • t
  • '); expect(this.dataset.template).toBe('boo'); }); }); + }); + + describe('#initialize', function() { + beforeEach(function() { + this.dataset = new Dataset({ name: '#initialize' }); + }); - describe('with local data', function () { + describe('when called without local, prefetch, or remote', function() { beforeEach(function() { - this.dataset = new Dataset({ name: 'local', local: fixtureData }); + this.fn = function() { this.dataset.initialize({}); }; }); - it('should process local data', function() { + it('should throw an error', function() { + expect(this.fn).toThrow(); + }); + }); + + describe('when called with local', function() { + beforeEach(function() { + this.dataset.initialize({ local: fixtureData }); + }); + + it('should process and merge the data', function() { expect(this.dataset.itemHash).toEqual(expectedItemHash); expect(this.dataset.adjacencyList).toEqual(expectedAdjacencyList); }); }); - describe('with prefetch data', function () { - describe('if available in storage', function() { + describe('when called with prefetch', function() { + describe('if data is available in storage', function() { beforeEach(function() { - spyOn(PersistentStorage.prototype, 'get') - .andCallFake(mockStorageFns.getHit); - - this.dataset = new Dataset({ - name: 'prefetch', - prefetch: '/prefetch.json' - }); + this.dataset.storage.get.andCallFake(mockStorageFns.getHit); + this.dataset.initialize({ prefetch: '/prefetch.json' }); }); it('should not make ajax request', function() { @@ -138,16 +139,26 @@ describe('Dataset', function() { }); }); - ['itemHash', 'adjacencyList', 'version', 'protocol'] - .forEach(function(key) { - describe('if ' + key + ' is stale or missing in storage', function() { - beforeEach(function() { - spyOn(PersistentStorage.prototype, 'get') - .andCallFake(mockStorageFns.getMissGenerator(key)); + describe('if data is not available in storage', function() { + // default ttl + var ttl = 3 * 24 * 60 * 60 * 1000; + + beforeEach(function() { + this.dataset.storage.get.andCallFake(mockStorageFns.getMiss); + }); - this.dataset = new Dataset({ - name: 'prefetch', - prefetch: '/prefetch.json' + describe('if filter was passed in', function() { + var filteredAdjacencyList = { f: ['filter'] }, + filteredItemHash = { + filter: { tokens: ['filter'], value: 'filter' } + }; + + beforeEach(function() { + this.dataset.initialize({ + prefetch: { + url: '/prefetch.json', + filter: function(data) { return ['filter']; } + } }); this.request = mostRecentAjaxRequest(); @@ -158,24 +169,74 @@ describe('Dataset', function() { expect(this.request).not.toBeNull(); }); - it('should process fetched data', function() { + it('should process and merge filtered data', function() { + expect(this.dataset.adjacencyList).toEqual(filteredAdjacencyList); + expect(this.dataset.itemHash).toEqual(filteredItemHash); + }); + + it('should store processed data in storage', function() { + expect(this.dataset.storage.set) + .toHaveBeenCalledWith('itemHash', filteredItemHash, ttl); + expect(this.dataset.storage.set) + .toHaveBeenCalledWith('adjacencyList', filteredAdjacencyList, ttl); + }); + + it('should store metadata in storage', function() { + expect(this.dataset.storage.set) + .toHaveBeenCalledWith('protocol', utils.getProtocol(), ttl); + expect(this.dataset.storage.set) + .toHaveBeenCalledWith('version', VERSION, ttl); + }); + }); + + describe('if filter was not passed in', function() { + beforeEach(function() { + this.dataset.initialize({ prefetch: '/prefetch.json' }); + + this.request = mostRecentAjaxRequest(); + this.request.response(prefetchResp); + }); + + it('should make ajax request', function() { + expect(this.request).not.toBeNull(); + }); + + it('should process and merge fetched data', function() { expect(this.dataset.itemHash).toEqual(expectedItemHash); expect(this.dataset.adjacencyList).toEqual(expectedAdjacencyList); }); + + it('should store processed data in storage', function() { + expect(this.dataset.storage.set) + .toHaveBeenCalledWith('itemHash', expectedItemHash, ttl); + expect(this.dataset.storage.set) + .toHaveBeenCalledWith('adjacencyList', expectedAdjacencyList, ttl); + }); + + it('should store metadata in storage', function() { + expect(this.dataset.storage.set) + .toHaveBeenCalledWith('protocol', utils.getProtocol(), ttl); + expect(this.dataset.storage.set) + .toHaveBeenCalledWith('version', VERSION, ttl); + }); }); }); }); + + describe('when called with remote', function() { + beforeEach(function() { + this.dataset.initialize({ remote: '/remote' }); + }); + + it('should initialize the transport', function() { + expect(Transport).toHaveBeenCalledWith('/remote'); + }); + }); }); describe('Datasource options', function() { beforeEach(function() { - spyOn(PersistentStorage.prototype, 'get') - .andCallFake(mockStorageFns.getHit); - - this.dataset = new Dataset({ - name: 'prefetch', - prefetch: '/prefetch.json' - }); + this.dataset = new Dataset({}).initialize({ local: fixtureData }); }); it('allow for a custom matching function to be defined', function() { @@ -208,18 +269,11 @@ describe('Dataset', function() { describe('Matching, ranking, combining, returning results', function() { beforeEach(function() { - spyOn(PersistentStorage.prototype, 'get') - .andCallFake(mockStorageFns.getHit); - - this.dataset = new Dataset({ - name: 'prefetch', - prefetch: '/prefetch.json' - }); + this.dataset = new Dataset({}) + .initialize({ local: fixtureData, remote: '/remote' }); }); it('network requests are not triggered with enough local results', function() { - spyOn(this.dataset.transport = { get: $.noop }, 'get'); - this.dataset.limit = 1; this.dataset.getSuggestions('c', function(items) { expect(items).toEqual([ @@ -295,13 +349,9 @@ describe('Dataset', function() { }); it('only returns unique ids when looking up potentially matching ids', function() { - this.dataset.adjacencyList = { - a: [1, 2, 3, 4], - b: [3, 4, 5, 6] - }; + this.dataset.adjacencyList = { a: [1, 2, 3, 4], b: [3, 4, 5, 6] }; expect(this.dataset._getPotentiallyMatchingIds(['a','b'])).toEqual([3, 4]); }); - }); describe('tokenization', function() { @@ -309,7 +359,7 @@ describe('Dataset', function() { var fixtureData = ['course-106', 'user_name', 'One-Two', 'two three']; beforeEach(function() { - this.dataset = new Dataset({ name: 'local', local: fixtureData }); + this.dataset = new Dataset({}).initialize({ local: fixtureData }); }); it('normalizes capitalization to match items', function() { @@ -354,7 +404,7 @@ describe('Dataset', function() { var fixtureData = [{ value: 'course-106', tokens: ['course-106'] }]; beforeEach(function() { - this.dataset = new Dataset({ name: 'local', local: fixtureData }); + this.dataset = new Dataset({}).initialize({ local: fixtureData }); }); it('matches items with dashes', function() { diff --git a/test/helpers/mock_components.js b/test/helpers/mock_components.js new file mode 100644 index 00000000..c4666693 --- /dev/null +++ b/test/helpers/mock_components.js @@ -0,0 +1,73 @@ +(function() { + var _RequestCache, _PersistentStorage, _Transport; + + $.extend(jasmine, { + RequestCache: { + useMock: function() { + var spec = jasmine.getEnv().currentSpec; + + spec.after(jasmine.RequestCache.uninstallMock); + jasmine.RequestCache.installMock(); + }, + + installMock: function() { + _RequestCache = RequestCache; + RequestCache = MockRequestCache; + }, + + uninstallMock: function() { + RequestCache = _RequestCache; + } + }, + + PersistentStorage: { + useMock: function() { + var spec = jasmine.getEnv().currentSpec; + + spec.after(jasmine.PersistentStorage.uninstallMock); + jasmine.PersistentStorage.installMock(); + }, + + installMock: function() { + _PersistentStorage = PersistentStorage; + PersistentStorage = jasmine.createSpy().andReturn({ + get: jasmine.createSpy(), + set: jasmine.createSpy(), + remove: jasmine.createSpy(), + clear: jasmine.createSpy(), + isExpired: jasmine.createSpy() + }); + }, + + uninstallMock: function() { + PersistentStorage = _PersistentStorage; + } + }, + + Transport: { + useMock: function() { + var spec = jasmine.getEnv().currentSpec; + + spec.after(jasmine.Transport.uninstallMock); + jasmine.Transport.installMock(); + }, + + installMock: function() { + _Transport = Transport; + Transport = jasmine.createSpy().andReturn({ + get: jasmine.createSpy() + }); + }, + + uninstallMock: function() { + Transport = _Transport; + } + } + }); + + function MockRequestCache() { + this.get = $.noop; + this.set = $.noop; + MockRequestCache.instance = this; + } +})(); diff --git a/test/helpers/mock_request_cache.js b/test/helpers/mock_request_cache.js deleted file mode 100644 index d83a7240..00000000 --- a/test/helpers/mock_request_cache.js +++ /dev/null @@ -1,27 +0,0 @@ -(function() { - var _RequestCache; - - jasmine.RequestCache = { - useMock: function() { - var spec = jasmine.getEnv().currentSpec; - - spec.after(jasmine.RequestCache.uninstallMock); - jasmine.RequestCache.installMock(); - }, - - installMock: function() { - _RequestCache = RequestCache; - RequestCache = MockRequestCache; - }, - - uninstallMock: function() { - RequestCache = _RequestCache; - } - }; - - function MockRequestCache() { - this.get = $.noop; - this.set = $.noop; - MockRequestCache.instance = this; - } -})(); diff --git a/test/transport_spec.js b/test/transport_spec.js index e641836c..fedb1aad 100644 --- a/test/transport_spec.js +++ b/test/transport_spec.js @@ -18,6 +18,8 @@ describe('Transport', function() { maxParallelRequests: 3 }); + // request cache is hidden in transport's closure + // so this is how we access it to spy on its methods this.requestCache = RequestCache.instance; spyOn(this.requestCache, 'get'); spyOn(this.requestCache, 'set'); @@ -162,13 +164,4 @@ describe('Transport', function() { }); }); }); - - // helper functions - // ---------------- - - function MockRequestCache() { - this.get = $.noop; - this.set = $.noop; - MockRequestCache.instance = this; - } });