var _ = require('underscore'); var Backbone = require('backbone'); var VisModel = require('../../../src/vis/vis'); var MapModel = require('../../../src/geo/map'); var DataviewModelBase = require('../../../src/dataviews/dataview-model-base'); var WindshaftFiltersBoundingBox = require('../../../src/windshaft/filters/bounding-box'); var AnalysisService = require('../../../src/analysis/analysis-service'); var MapModelBoundingBoxAdapter = require('../../../src/geo/adapters/map-model-bounding-box-adapter'); var MockFactory = require('../../helpers/mockFactory'); var createEngine = require('../fixtures/engine.fixture.js'); var fakeCamshaftReference = { getSourceNamesForAnalysisType: function (analysisType) { var map = { 'source': [], 'trade-area': ['source'], 'estimated-population': ['source'], 'point-in-polygon': ['points_source', 'polygons_source'], 'union': ['source'] }; if (!map[analysisType]) { throw new Error('analysis type ' + analysisType + ' not supported'); } return map[analysisType]; }, getParamNamesForAnalysisType: function (analysisType) { var map = { 'source': ['query'], 'trade-area': ['kind', 'time'], 'estimated-population': ['columnName'], 'point-in-polygon': [], 'union': ['join_on'] }; if (!map[analysisType]) { throw new Error('analysis type ' + analysisType + ' not supported'); } return map[analysisType]; } }; describe('dataviews/dataview-model-base', function () { var engineMock; var apiKeyQueryParam; beforeEach(function () { this.map = new MapModel(null, { layersFactory: {} }); this.map.setBounds([[102, 200], [300, 400]]); this.vis = new VisModel(); engineMock = createEngine(); this.vis._layersCollection = engineMock._layersCollection; this.vis._dataviewsCollection = engineMock._dataviewsCollection; apiKeyQueryParam = 'api_key=' + engineMock.getApiKey(); this.vis._onMapInstantiatedForTheFirstTime(); this.analysisService = new AnalysisService({ engine: engineMock, camshaftReference: fakeCamshaftReference }); this.source = this.analysisService.analyse({ id: 'a0', type: 'source' }); this.analysisNodes = this.source.getNodesCollection(); // Disable debounce spyOn(_, 'debounce').and.callFake(function (func) { return function () { func.apply(this, arguments); }; }); this.model = new DataviewModelBase({ source: this.source }, { engine: engineMock, bboxFilter: new WindshaftFiltersBoundingBox(new MapModelBoundingBoxAdapter(this.map)) }); this.model.toJSON = jasmine.createSpy('toJSON').and.returnValue({}); engineMock._dataviewsCollection.add(this.model); this.model._bboxFilter._stopBinds(); this.model._bboxFilter._initBinds(); }); describe('url', function () { it('should include the bbox param', function () { this.map.setBounds([['south', 'west'], ['north', 'east']]); this.model.set('url', 'http://example.com'); expect(this.model.url()).toEqual('http://example.com?bbox=west,south,east,north&' + apiKeyQueryParam); }); it('should allow subclasses to define specific URL params', function () { this.map.setBounds([['south', 'west'], ['north', 'east']]); this.model.set('url', 'http://example.com'); spyOn(this.model, '_getDataviewSpecificURLParams').and.returnValue([ 'a=b', 'c=d' ]); expect(this.model.url()).toEqual('http://example.com?bbox=west,south,east,north&a=b&c=d&' + apiKeyQueryParam); }); it('should append an api_key param if apiKey attr is present (and not use the auth_token)', function () { this.map.setBounds([['south', 'west'], ['north', 'east']]); this.model.set({ url: 'http://example.com', apiKey: 'THE_API_KEY', authToken: 'THE_AUTH_TOKEN' }); spyOn(this.model, '_getDataviewSpecificURLParams').and.returnValue([ 'a=b', 'c=d' ]); expect(this.model.url()).toEqual('http://example.com?bbox=west,south,east,north&a=b&c=d&' + apiKeyQueryParam); }); it('should append an auth_token param if authToken is present', function () { this.map.setBounds([['south', 'west'], ['north', 'east']]); this.model.set({ url: 'http://example.com' }); delete this.model._engine._windshaftSettings.apiKey; spyOn(this.model, '_getDataviewSpecificURLParams').and.returnValue([ 'a=b', 'c=d' ]); expect(this.model.url()).toEqual('http://example.com?bbox=west,south,east,north&a=b&c=d&auth_token[]=fabada&auth_token[]=coffee'); }); }); describe('when url changes', function () { beforeEach(function () { spyOn(this.model, 'fetch'); spyOn(this.model, 'listenTo'); spyOn(this.model, 'on'); }); describe('when map view bounds are ready', function () { beforeEach(function () { this.model.set('url', 'http://example.com'); }); it('should fetch', function () { expect(this.model.fetch).toHaveBeenCalled(); }); describe('when fetch succeeds', function () { beforeEach(function () { this.model.fetch.calls.argsFor(0)[0].success(); }); it('should change bounds', function () { expect(this.model.on.calls.argsFor(0)[0]).toEqual('change:sync_on_bbox_change'); expect(this.model.on.calls.argsFor(1)[0]).toEqual('change:url'); expect(this.model.on.calls.argsFor(2)[0]).toEqual('change:enabled'); }); }); }); describe('when map view bounds are NOT ready', function () { beforeEach(function () { spyOn(this.model._bboxFilter, 'areBoundsAvailable').and.returnValue(false); }); describe('when sync_on_bbox_change is true', function () { beforeEach(function () { this.model.set({ 'sync_on_bbox_change': true, 'url': 'http://example.com' }); }); it('should wait until view bounds are ready', function () { expect(this.model.fetch).not.toHaveBeenCalled(); this.map.setBounds([[5, 6], [7, 8]]); expect(this.model.fetch).toHaveBeenCalled(); }); }); describe('when sync_on_bbox_change is false', function () { beforeEach(function () { this.model.set({ 'sync_on_bbox_change': false, 'url': 'http://example.com' }); }); it('should fetch', function () { expect(this.model.fetch).toHaveBeenCalled(); }); }); }); }); describe('after first successful fetch', function () { beforeEach(function () { this.model.fetch = function (opts) { opts.success(); }; this.model.set('url', 'newurl'); }); it('should not fetch new data when url changes and dataview is disabled', function () { this.model.set('enabled', false); spyOn(this.model, 'fetch'); this.model.trigger('change:url', this.model); expect(this.model.fetch).not.toHaveBeenCalled(); }); it('should fetch if url changes and forceFetch option is true, no matter rest of variables', function () { this.model.set('enabled', false); spyOn(this.model, 'fetch'); this.model.trigger('change:url', this.model, {}, { forceFetch: true }); expect(this.model.fetch).toHaveBeenCalled(); this.model.fetch.calls.reset(); this.model.trigger('change:url', this.model, {}, { forceFetch: false }); expect(this.model.fetch).not.toHaveBeenCalled(); }); it('should fetch if url changes and sourceId is not defined', function () { spyOn(this.model, 'fetch'); this.model.set('url', 'http://somethingelese.com'); expect(this.model.fetch).toHaveBeenCalled(); }); describe('when change:url has a sourceId option', function () { beforeEach(function () { var analysisA = this.analysisService.analyse({ id: 'a2', type: 'estimated-population', params: { columnName: 'estimated_people', source: { id: 'a1', type: 'trade-area', params: { kind: 'walk', time: 300, source: { id: 'a0', type: 'source', params: { query: 'select * from subway_stops' } } } } } }); var analysisNodes = analysisA.getNodesCollection(); this.model.set('source', analysisNodes.get('a1'), { silent: true }); spyOn(this.model, 'fetch'); }); it("should fetch if sourceId matches the dataview's source", function () { this.model.set('url', 'http://somethingelese.com', { sourceId: 'a1' }); expect(this.model.fetch).toHaveBeenCalled(); }); it("should fetch if sourceId is a node that affects the dataview's source", function () { this.model.set('url', 'http://somethingelese.com', { sourceId: 'a0' }); expect(this.model.fetch).toHaveBeenCalled(); }); it("should NOT fetch if sourceId is a node that doesn't affect the dataview's source", function () { this.model.set('url', 'http://somethingelese.com', { sourceId: 'a2' }); expect(this.model.fetch).not.toHaveBeenCalled(); }); }); }); describe('when enabled is changed to true from false', function () { beforeEach(function () { this.model.fetch = function (opts) { opts.success(); }; this.model.set('url', 'http://example.com'); this.model.set('enabled', false); spyOn(this.model, 'fetch'); }); it('should NOT fetch if nothing has changed', function () { this.model.set('enabled', true); expect(this.model.fetch).not.toHaveBeenCalled(); }); it('should fetch if the bounding box have changed while the dataview was disabled', function () { // Map bounds have changed this.map.setBounds([[102, 200], [300, 400]]); this.map.trigger('change:center'); this.model.set('enabled', true); expect(this.model.fetch).toHaveBeenCalled(); this.model.fetch.calls.reset(); // Disable and enable again this.model.set('enabled', false); this.model.set('enabled', true); expect(this.model.fetch).not.toHaveBeenCalled(); }); it('should NOT fetch if the bounding box have changed while the dataview was disabled and sync_on_bbox_change is disabled', function () { this.model.set('sync_on_bbox_change', false); // To get the full range of data expect(this.model.fetch).toHaveBeenCalled(); this.model.fetch.calls.reset(); // Map bounds have changed this.map.setBounds([[102, 200], [300, 400]]); this.map.trigger('change:center'); this.model.set('enabled', true); expect(this.model.fetch).not.toHaveBeenCalled(); }); it('should fetch if URL has changed while the dataview was disabled', function () { this.model.set('url', 'http://somethingelse.com'); this.model.set('enabled', true); expect(this.model.fetch).toHaveBeenCalled(); this.model.fetch.calls.reset(); // Disable and enable again this.model.set('enabled', false); this.model.set('enabled', true); expect(this.model.fetch).not.toHaveBeenCalled(); }); }); describe('bindings to map bounds', function () { beforeEach(function () { this.model.fetch = function (opts) { opts.success(); }; this.model.set('url', 'http://example.com'); spyOn(this.model, 'fetch'); }); it('should fetch when the bounding box has changed', function () { this.map.setBounds([[102, 200], [300, 400]]); this.map.trigger('change:center'); expect(this.model.fetch).toHaveBeenCalled(); }); it('should NOT fetch when the bounding box has changed and the dataview is not enabled', function () { this.model.set('enabled', false); this.map.setBounds([[102, 200], [300, 400]]); this.map.trigger('change:center'); expect(this.model.fetch).not.toHaveBeenCalled(); }); it('should NOT fetch when the bounding box has changed and the dataview has sync_on_bbox_change disabled', function () { this.model.set('sync_on_bbox_change', false); // To get the full range of data expect(this.model.fetch).toHaveBeenCalled(); this.model.fetch.calls.reset(); this.map.setBounds([[102, 200], [300, 400]]); this.map.trigger('change:center'); expect(this.model.fetch).not.toHaveBeenCalled(); }); }); describe('bindings to the filter', function () { it('should reload the map by default when the filter changes', function () { var filter = new Backbone.Model(); new DataviewModelBase({ // eslint-disable-line source: this.source }, { map: this.map, engine: engineMock, filter: filter }); // Filter changes filter.trigger('change', filter); expect(engineMock.reload).toHaveBeenCalledWith({ sourceId: 'a0' }); }); }); describe('.remove', function () { beforeEach(function () { this.removeSpy = jasmine.createSpy('remove'); this.model.once('destroy', this.removeSpy); spyOn(this.model, 'stopListening'); spyOn(this.source, 'off').and.callThrough(); this.model.filter = jasmine.createSpyObj('filter', ['remove', 'isEmpty']); this.model.filter.isEmpty.and.returnValue(false); this.model.remove(); }); it('should trigger a destroy event', function () { expect(this.removeSpy).toHaveBeenCalledWith(this.model); }); it('should stop listening to events', function () { expect(this.model.stopListening).toHaveBeenCalled(); expect(this.source.off).toHaveBeenCalledWith('change:status', jasmine.any(Function), this.model); }); }); describe('.update', function () { it('should only update the attrs set on ATTRS_NAMES', function () { this.model.update({ foo: 'bar' }); expect(this.model.changedAttributes()).toBe(false); expect(this.model.get('sync_on_bbox_change')).toBe(true); this.model.update({ sync_on_bbox_change: false, foo: 'bar' }); expect(this.model.changedAttributes()).toEqual({ sync_on_bbox_change: false }); }); }); describe('.fetch', function () { it('should trigger a loading event', function () { spyOn(this.model, 'trigger'); this.model.fetch(); expect(this.model.trigger).toHaveBeenCalledWith('loading', this.model); }); }); describe('getSourceType', function () { it('should return the type of the source', function () { var dataview = new DataviewModelBase({ source: this.analysisNodes.get('a0') }, { map: this.map, engine: engineMock }); expect(dataview.getSourceType()).toEqual('source'); }); }); describe('isSourceType', function () { it('should return true if the source type is source', function () { var dataview = new DataviewModelBase({ source: this.analysisNodes.get('a0') }, { map: this.map, engine: engineMock }); dataview.getSourceType = function () { return 'source'; }; expect(dataview.isSourceType()).toBe(true); }); }); describe('when source type is not source', function () { describe('isSourceType', function () { it('should return false', function () { var dataview = new DataviewModelBase({ source: this.analysisNodes.get('a0') }, { map: this.map, engine: engineMock }); dataview.getSourceType = function () { return 'sampling'; }; expect(dataview.isSourceType()).toBe(false); }); }); }); describe('getSourceId', function () { it('should return the id of the source', function () { var dataview = new DataviewModelBase({ source: this.source }, { // eslint-disable-line map: this.map, engine: engineMock }); expect(dataview.getSourceId()).toEqual('a0'); }); }); describe('when analysis changes status', function () { beforeEach(function () { this.source.isLoading = jasmine.createSpy('a0.isLoading'); this.source.isFailed = jasmine.createSpy('a0.isFailed'); this.model.on({ loading: this.loadingSpy = jasmine.createSpy('loading'), statusError: this.errorSpy = jasmine.createSpy('failed') }); }); sharedTestsForAnalysisEvents(); describe('when changed source', function () { beforeEach(function () { this.model.set('source', this.source); }); sharedTestsForAnalysisEvents(); }); }); describe('source references', function () { var source; var dataview; beforeEach(function () { source = MockFactory.createAnalysisModel({ id: 'a0' }); dataview = new DataviewModelBase({ source: source }, { map: this.map, engine: engineMock }); }); describe('when dataview is initialized', function () { it('should mark source as referenced', function () { expect(source.isSourceOf(dataview)).toBe(true); }); }); describe('when dataview is removed', function () { it('should unmark source as referenced', function () { expect(source.isSourceOf(dataview)).toBe(true); dataview.remove(); expect(source.isSourceOf(dataview)).toBe(false); }); }); }); describe('._parseError', function () { var source; var dataview; beforeEach(function () { source = MockFactory.createAnalysisModel({ id: 'a0' }); dataview = new DataviewModelBase({ source: source }, { map: this.map, engine: engineMock }); }); it('should pass the response directly to the parser and get the first returned error', function () { var response = { responseJSON: { errors: ['an error'] } }; var error = dataview._parseError(response); expect(error.message).toEqual('an error'); }); }); describe('._fetch', function () { describe('when request is success', function () { beforeEach(function () { this.model.fetch = function (opts) { opts.success(); }; }); it('sets _hasBinds to true', function () { expect(this.model._hasBinds).toBe(false); this.model._fetch(); expect(this.model._hasBinds).toBe(true); }); it('calls ._onChangeBinds', function () { spyOn(this.model, '_onChangeBinds'); this.model._fetch(); expect(this.model._onChangeBinds).toHaveBeenCalled(); }); }); }); describe('._onMapBoundsChanged', function () { describe('when _shouldFetchOnBoundingBoxChange is true', function () { it('calls ._fetch', function () { spyOn(this.model, '_shouldFetchOnBoundingBoxChange').and.returnValue(true); spyOn(this.model, '_fetch'); this.model._onMapBoundsChanged(); expect(this.model._fetch).toHaveBeenCalled(); }); }); }); }); function sharedTestsForAnalysisEvents () { describe('should trigger the event according to state', function () { it('should trigger loading event', function () { this.loadingSpy.calls.reset(); this.errorSpy.calls.reset(); this.source.isLoading.and.returnValue(true); this.source.set('status', 'whatever'); expect(this.loadingSpy).toHaveBeenCalled(); expect(this.errorSpy).not.toHaveBeenCalled(); this.loadingSpy.calls.reset(); this.errorSpy.calls.reset(); this.source.isLoading.and.returnValue(false); this.source.isFailed.and.returnValue(true); this.source.set({ status: 'failed', error: this.err = {} }); expect(this.loadingSpy).not.toHaveBeenCalled(); expect(this.errorSpy).toHaveBeenCalledWith(this.model, this.err); }); }); }