var Backbone = require('backbone'); var _ = require('underscore'); var CategoryDataviewModel = require('../../../src/dataviews/category-dataview-model'); var WindshaftFiltersCategory = require('../../../src/windshaft/filters/category'); var WindshaftFiltersBoundingBox = require('../../../src/windshaft/filters/bounding-box'); var WindshaftFiltersCircle = require('../../../src/windshaft/filters/circle'); var WindshaftFiltersPolygon = require('../../../src/windshaft/filters/polygon'); var AnalysisService = require('../../../src/analysis/analysis-service'); var MapModelBoundingBoxAdapter = require('../../../src/geo/adapters/map-model-bounding-box-adapter'); var createEngine = require('../fixtures/engine.fixture.js'); describe('dataviews/category-dataview-model', function () { var engineMock; var apiKey = 'API_KEY'; var apiKeyQueryParam = 'api_key=' + apiKey; beforeEach(function () { this.map = new Backbone.Model(); this.map.getViewBounds = jasmine.createSpy(); engineMock = createEngine({ apiKey: apiKey }); this.map.getViewBounds.and.returnValue([[1, 2], [3, 4]]); var analysisDefinition = { id: 'a0', type: 'source', params: { query: 'SELECT * FROM blairbnb_listings' } }; var analysisService = new AnalysisService({ engine: engineMock }); this.source = analysisService.analyse(analysisDefinition); spyOn(_, 'debounce').and.callFake(function (func) { return function () { func.apply(this, arguments); }; }); this.model = new CategoryDataviewModel({ source: this.source }, { engine: engineMock, filter: new WindshaftFiltersCategory(), bboxFilter: new WindshaftFiltersBoundingBox(new MapModelBoundingBoxAdapter(this.map)) }); }); it('should reload map and force fetch on changing attrs', function () { engineMock.reload.calls.reset(); this.model.set('column', 'random_col'); expect(engineMock.reload).toHaveBeenCalledWith({ forceFetch: true, sourceId: 'a0' }); engineMock.reload.calls.reset(); this.model.set('aggregation', 'count'); expect(engineMock.reload).toHaveBeenCalledWith({ forceFetch: true, sourceId: 'a0' }); engineMock.reload.calls.reset(); this.model.set('aggregation_column', 'other'); expect(engineMock.reload).toHaveBeenCalledWith({ forceFetch: true, sourceId: 'a0' }); }); it('should define several internal models/collections', function () { expect(this.model._data).toBeDefined(); expect(this.model._searchModel).toBeDefined(); expect(this.model.filter).toBeDefined(); }); it('should set the api_key attribute on the internal models', function () { this.model = new CategoryDataviewModel({ source: this.source }, { map: this.map, engine: engineMock, layer: jasmine.createSpyObj('layer', ['get']), filter: new WindshaftFiltersCategory() }); expect(this.model._searchModel.get('apiKey')).toEqual(apiKey); expect(this.model._rangeModel.get('apiKey')).toEqual(apiKey); }); describe('.url', function () { it('should include the bbox, own_filter and categories parameters', function () { expect(this.model.set('url', 'http://example.com')); expect(this.model.url()).toEqual('http://example.com?bbox=2,1,4,3&own_filter=0&categories=6&' + apiKeyQueryParam); this.model.set('filterEnabled', true); expect(this.model.url()).toEqual('http://example.com?bbox=2,1,4,3&own_filter=1&categories=6&' + apiKeyQueryParam); this.model.set('filterEnabled', false); expect(this.model.url()).toEqual('http://example.com?bbox=2,1,4,3&own_filter=0&categories=6&' + apiKeyQueryParam); this.model.set('categories', 1); expect(this.model.url()).toEqual('http://example.com?bbox=2,1,4,3&own_filter=0&categories=1&' + apiKeyQueryParam); }); it('should include circle filter, plus other required params', function () { var filter = new WindshaftFiltersCircle(); var circle = {lat: 1, lng: 2, radius: 3}; filter.setCircle(circle); this.model = new CategoryDataviewModel({ source: this.source }, { engine: engineMock, filter: new WindshaftFiltersCategory(), circleFilter: filter }); // DataviewModel defaults set this prop to true, even for cases like this not requiring passing a bbox filter this.model.set('sync_on_bbox_change', false); var circleEncoded = encodeURIComponent(JSON.stringify(circle)); expect(this.model.set('url', 'http://example.com')); expect(this.model.url()).toEqual('http://example.com?circle=' + circleEncoded + '&own_filter=0&categories=6&' + apiKeyQueryParam); }); it('should update circle filter', function () { var filter = new WindshaftFiltersCircle(); var circle = {lat: 1, lng: 2, radius: 3}; filter.setCircle(circle); this.model = new CategoryDataviewModel({ source: this.source }, { engine: engineMock, filter: new WindshaftFiltersCategory(), circleFilter: filter }); // DataviewModel defaults set this prop to true, even for cases like this not requiring passing a bbox filter this.model.set('sync_on_bbox_change', false); // updated! var updatedCircle = {lat: 10, lng: 20, radius: 30}; filter.setCircle(updatedCircle); var updatedCircleEncoded = encodeURIComponent(JSON.stringify(updatedCircle)); expect(this.model.set('url', 'http://example.com')); expect(this.model.url()).toEqual('http://example.com?circle=' + updatedCircleEncoded + '&own_filter=0&categories=6&' + apiKeyQueryParam); }); it('should include polygon filter, plus other required params', function () { var filter = new WindshaftFiltersPolygon(); var polygon = { type: 'Polygon', coordinates: [[1, 2], [3, 4], [5, 6], [1, 2]] }; filter.setPolygon(polygon); this.model = new CategoryDataviewModel({ source: this.source }, { engine: engineMock, filter: new WindshaftFiltersCategory(), polygonFilter: filter }); // DataviewModel defaults set this prop to true, even for cases like this not requiring passing a bbox filter this.model.set('sync_on_bbox_change', false); var polygonEncoded = encodeURIComponent(JSON.stringify(polygon)); expect(this.model.set('url', 'http://example.com')); expect(this.model.url()).toEqual('http://example.com?polygon=' + polygonEncoded + '&own_filter=0&categories=6&' + apiKeyQueryParam); }); it('should update polygon filter', function () { var filter = new WindshaftFiltersPolygon(); var polygon = { type: 'Polygon', coordinates: [[1, 2], [3, 4], [5, 6], [1, 2]] }; filter.setPolygon(polygon); this.model = new CategoryDataviewModel({ source: this.source }, { engine: engineMock, filter: new WindshaftFiltersCategory(), polygonFilter: filter }); // DataviewModel defaults set this prop to true, even for cases like this not requiring passing a bbox filter this.model.set('sync_on_bbox_change', false); // updated! var updatedPolygon = { type: 'Polygon', coordinates: [[10, 20], [30, 40], [50, 60], [10, 20]] }; filter.setPolygon(updatedPolygon); var updatedPolygonEncoded = encodeURIComponent(JSON.stringify(updatedPolygon)); expect(this.model.set('url', 'http://example.com')); expect(this.model.url()).toEqual('http://example.com?polygon=' + updatedPolygonEncoded + '&own_filter=0&categories=6&' + apiKeyQueryParam); }); }); describe('binds', function () { beforeEach(function () { this.model.set({ url: 'http://heytest.io' }); }); describe('url', function () { beforeEach(function () { spyOn(this.model, 'fetch'); spyOn(this.model._searchModel, 'fetch'); spyOn(this.model._rangeModel, 'fetch'); }); it('should set search url when it changes', function () { expect(this.model._searchModel.get('url')).toBe('http://heytest.io'); expect(this.model._searchModel.url()).toBe('http://heytest.io/search?q=&' + apiKeyQueryParam); }); it('should set rangeModel url when it changes', function () { expect(this.model._rangeModel.get('url')).toBe('http://heytest.io'); expect(this.model._rangeModel.url()).toBe('http://heytest.io?' + apiKeyQueryParam); }); }); describe('search events dispatcher', function () { var eventNames = ['loading', 'loaded', 'error']; _.each(eventNames, function (eventName) { it("should re-trigger the '" + eventName + "' event", function () { var spyObj = jasmine.createSpy(eventName); this.model.bind(eventName, spyObj); this.model._searchModel.trigger(eventName); expect(spyObj).toHaveBeenCalled(); }); }, this); describe('on search data change', function () { beforeEach(function () { spyOn(this.model, 'trigger'); this.model.filter.accept(['hey']); this.model._searchModel.setData([{ name: 'hey', value: 1 }, { name: 'vamos', value: 2 }, { name: 'neno', value: 3 }]); }); it('should check if search results are already selected or not', function () { var data = this.model.getSearchResult(); expect(data.size()).toBe(3); var selectedCategories = data.where({ selected: true }); var selectedCategory = selectedCategories[0]; expect(_.size(selectedCategories)).toBe(1); expect(selectedCategory.get('name')).toBe('hey'); }); it('should trigger searchData event', function () { expect(this.model.trigger).toHaveBeenCalledWith('change:searchData', this.model); }); }); }); describe('range model', function () { it('should set totalCount when rangeModel has changed', function () { expect(this.model.get('totalCount')).toBeUndefined(); this.model._rangeModel.set({ totalCount: 1000 }); expect(this.model.get('totalCount')).toBe(1000); }); it('should set categoriesCount when rangeModel has changed', function () { expect(this.model.get('categoriesCount')).toBeUndefined(); this.model._rangeModel.set({ categoriesCount: 123 }); expect(this.model.get('categoriesCount')).toBe(123); }); }); }); describe('bindings to map bounds', function () { beforeEach(function () { // Disable debounce this.model._bboxFilter._stopBinds(); this.model._bboxFilter._initBinds(); this.model.fetch = function (opts) { opts && opts.success(); }; this.model.set('url', 'http://example.com'); }); it('should fetch when the bounding box has changed', function () { spyOn(this.model._searchModel, 'fetchIfSearchIsApplied'); this.map.getViewBounds.and.returnValue([200, 200], [300, 400]); this.map.trigger('change:center'); expect(this.model._searchModel.fetchIfSearchIsApplied).toHaveBeenCalled(); }); }); describe('search model helpers', function () { it('should clean search properly', function () { spyOn(this.model._searchModel, 'resetData'); this.model.cleanSearch(); expect(this.model._searchModel.resetData).toHaveBeenCalled(); }); describe('setupSearch', function () { beforeEach(function () { spyOn(this.model._searchModel, 'setData').and.callThrough(); }); it('should not setup search if search is already applied', function () { spyOn(this.model, 'isSearchApplied').and.returnValue(true); this.model.setupSearch(); expect(this.model._searchModel.setData).not.toHaveBeenCalled(); }); it('should setup search if it is gonna be enabled', function () { spyOn(this.model, 'isSearchApplied').and.returnValue(false); _parseData(this.model, _generateData(3)); this.model.filter.accept(['4', '5', '6']); this.model.setupSearch(); expect(this.model._searchModel.setData).toHaveBeenCalled(); expect(this.model.getSearchCount()).toBe(3); }); }); }); it('should refresh its own data only if the search is not applied', function () { spyOn(this.model, 'fetch'); spyOn(this.model._searchModel, 'fetch'); this.model.refresh(); expect(this.model.fetch.calls.count()).toEqual(1); expect(this.model.fetch).toHaveBeenCalled(); expect(this.model._searchModel.fetch).not.toHaveBeenCalled(); spyOn(this.model, 'isSearchApplied').and.returnValue(true); this.model.refresh(); expect(this.model._searchModel.fetch).toHaveBeenCalled(); expect(this.model.fetch.calls.count()).toEqual(1); }); describe('filters over data', function () { beforeEach(function () { this.model._data.reset([{ name: 'one', value: 1 }, { name: 'buddy', value: 2 }, { name: 'neno', value: 3 }]); }); describe('.numberOfAcceptedCategories', function () { it('should count accepted categories over the current data', function () { this.model.filter.accept('vamos'); expect(this.model.numberOfAcceptedCategories()).toBe(0); this.model.filter.accept('buddy'); expect(this.model.numberOfAcceptedCategories()).toBe(1); this.model.filter.reject('neno'); expect(this.model.numberOfAcceptedCategories()).toBe(2); this.model._data.reset([]); expect(this.model.numberOfAcceptedCategories()).toBe(0); }); }); describe('.numberOfRejectedCategories', function () { it('should count rejected categories over the current data', function () { this.model.filter.reject('vamos'); expect(this.model.numberOfRejectedCategories()).toBe(0); this.model.filter.reject('buddy'); expect(this.model.numberOfRejectedCategories()).toBe(1); this.model.filter.accept('neno'); expect(this.model.numberOfRejectedCategories()).toBe(1); this.model._data.reset([]); expect(this.model.numberOfRejectedCategories()).toBe(0); }); }); }); describe('.parse', function () { it('should change internal data collection when parse is called', function () { var resetSpy = jasmine.createSpy('reset'); this.model._data.bind('reset', resetSpy); _parseData(this.model, _generateData(2)); expect(resetSpy).toHaveBeenCalled(); }); describe('when filter is disabled', function () { it('should NOT add categories that are accepted when they are not present in the new categories', function () { this.model.filter.accept('Madrid'); this.model.disableFilter(); _parseData(this.model, _.map(['Barcelona'], function (v) { return { category: v, value: 1 }; })); var categories = this.model.get('data'); expect(categories.length).toEqual(1); expect(categories[0].name).toEqual('Barcelona'); }); }); describe('when filter is enabled', function () { it('should add categories that are accepted when they are not present in the new categories', function () { this.model.filter.accept('Madrid'); this.model.enableFilter(); _parseData(this.model, _.map(['Barcelona'], function (v) { return { category: v, value: 1 }; })); var categories = this.model.get('data'); expect(categories.length).toEqual(2); expect(categories[0].name).toEqual('Barcelona'); expect(categories[1].name).toEqual('Madrid'); }); }); }); describe('.update', function () { beforeEach(function () { expect(this.model.get('foo')).toBeUndefined(); expect(this.model.get('sync_on_bbox_change')).toBe(true); expect(this.model.get('aggregation')).not.toEqual('sum'); this.model.update({ sync_on_bbox_change: false, aggregation: 'sum', foo: 'bar' }); }); it('should allow to set attrs but only the defined ones', function () { expect(this.model.get('sync_on_bbox_change')).toBe(false); expect(this.model.get('aggregation')).toEqual('sum'); expect(this.model.get('foo')).toBeUndefined(); }); }); describe('.getCount', function () { it('returns the total number of categories', function () { this.model.set('categoriesCount', 99999); expect(this.model.getCount()).toEqual(99999); }); }); }); function _generateData (n) { return _.times(n, function (i) { return { category: i, value: 2 }; }); } function _parseData (model, categories) { model.sync = function (method, model, options) { options.success({ 'categories': categories }); }; model.fetch(); }