2708 lines
96 KiB
JavaScript
2708 lines
96 KiB
JavaScript
|
var $ = require('jquery');
|
||
|
var _ = require('underscore');
|
||
|
var Backbone = require('backbone');
|
||
|
var CDB = require('internal-carto.js');
|
||
|
var MetricsTracker = require('builder/components/metrics/metrics-tracker');
|
||
|
var ConfigModel = require('builder/data/config-model');
|
||
|
var UserModel = require('builder/data/user-model');
|
||
|
var LayerDefinitionModel = require('builder/data/layer-definition-model');
|
||
|
var AnalysisDefinitionNodesCollection = require('builder/data/analysis-definition-nodes-collection');
|
||
|
var AnalysisDefinitionsCollection = require('builder/data/analysis-definitions-collection');
|
||
|
var LayerDefinitionsCollection = require('builder/data/layer-definitions-collection');
|
||
|
var WidgetDefinitionsCollection = require('builder/data/widget-definitions-collection');
|
||
|
var AnalysisFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/base-analysis-form-model');
|
||
|
var WidgetOptionModel = require('builder/components/modals/add-widgets/widget-option-model');
|
||
|
var UserActions = require('builder/data/user-actions');
|
||
|
var AreaOfInfluenceFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/area-of-influence-form-model');
|
||
|
var CategoryWidgetOptionModel = require('builder/components/modals/add-widgets/category/category-option-model');
|
||
|
var FilterByNodeColumnFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/filter-by-node-column');
|
||
|
var TableModel = require('builder/data/table-model');
|
||
|
var AnalysisSourceOptionsModel = require('builder/editor/layers/layer-content-views/analyses/analysis-source-options-model');
|
||
|
var analyses = require('builder/data/analyses');
|
||
|
|
||
|
describe('builder/data/user-actions', function () {
|
||
|
var interceptAjaxCall;
|
||
|
|
||
|
beforeEach(function () {
|
||
|
interceptAjaxCall = null;
|
||
|
|
||
|
spyOn(MetricsTracker, 'track');
|
||
|
|
||
|
this.configModel = new ConfigModel({
|
||
|
base_url: '/u/pepe',
|
||
|
user_name: 'pepe'
|
||
|
});
|
||
|
|
||
|
this.userModel = new UserModel({
|
||
|
limits: { max_layers: 4 }
|
||
|
}, {
|
||
|
configModel: this.configModel
|
||
|
});
|
||
|
|
||
|
this.analysisDefinitionNodesCollection = new AnalysisDefinitionNodesCollection(null, {
|
||
|
configModel: this.configModel,
|
||
|
userModel: this.userModel
|
||
|
});
|
||
|
this.analysisDefinitionsCollection = new AnalysisDefinitionsCollection(null, {
|
||
|
configModel: this.configModel,
|
||
|
vizId: 'viz-123',
|
||
|
layerDefinitionsCollection: new Backbone.Collection(),
|
||
|
analysisDefinitionNodesCollection: this.analysisDefinitionNodesCollection
|
||
|
});
|
||
|
|
||
|
this.layerDefinitionsCollection = new LayerDefinitionsCollection(null, {
|
||
|
configModel: this.configModel,
|
||
|
userModel: this.userModel,
|
||
|
analysisDefinitionNodesCollection: this.analysisDefinitionNodesCollection,
|
||
|
mapId: 'map-123',
|
||
|
stateDefinitionModel: {}
|
||
|
});
|
||
|
|
||
|
this.widgetDefinitionsCollection = new WidgetDefinitionsCollection(null, {
|
||
|
configModel: this.configModel,
|
||
|
layerDefinitionsCollection: this.layerDefinitionsCollection,
|
||
|
analysisDefinitionNodesCollection: this.analysisDefinitionNodesCollection,
|
||
|
mapId: 'map-123'
|
||
|
});
|
||
|
|
||
|
this.userActions = UserActions({
|
||
|
userModel: this.userModel,
|
||
|
analysisDefinitionNodesCollection: this.analysisDefinitionNodesCollection,
|
||
|
analysisDefinitionsCollection: this.analysisDefinitionsCollection,
|
||
|
layerDefinitionsCollection: this.layerDefinitionsCollection,
|
||
|
widgetDefinitionsCollection: this.widgetDefinitionsCollection
|
||
|
});
|
||
|
|
||
|
// Fake requests working, by default
|
||
|
spyOn(Backbone, 'ajax').and.callFake(function (params) {
|
||
|
interceptAjaxCall && interceptAjaxCall(params);
|
||
|
return {
|
||
|
always: function (cb) {
|
||
|
cb();
|
||
|
return this;
|
||
|
},
|
||
|
done: function (cb) {
|
||
|
cb();
|
||
|
return this;
|
||
|
},
|
||
|
fail: function (cb) {
|
||
|
cb();
|
||
|
return this;
|
||
|
}
|
||
|
};
|
||
|
});
|
||
|
});
|
||
|
|
||
|
afterEach(function () {
|
||
|
interceptAjaxCall = null;
|
||
|
});
|
||
|
|
||
|
describe('.createAnalysisNode', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.analysisDefinitionsCollection, 'create');
|
||
|
|
||
|
this.a0 = this.analysisDefinitionNodesCollection.add({
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: {
|
||
|
query: 'SELECT * FROM a_table'
|
||
|
},
|
||
|
options: {
|
||
|
table_name: 'a_table'
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given a layer that has no analysis yet', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'layerA',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
table_name: 'alice',
|
||
|
letter: 'a',
|
||
|
source: 'a0'
|
||
|
}
|
||
|
});
|
||
|
spyOn(this.layerDefModel, 'save').and.callThrough();
|
||
|
|
||
|
var nodeAttrs = {
|
||
|
id: 'a1',
|
||
|
type: 'trade-area',
|
||
|
source: 'a0',
|
||
|
kind: 'walk',
|
||
|
time: 123
|
||
|
};
|
||
|
|
||
|
spyOn(this.userActions, '_resetStylePerNode');
|
||
|
this.nodeDefModel = this.userActions.createAnalysisNode(nodeAttrs, this.layerDefModel);
|
||
|
});
|
||
|
|
||
|
it('should create a new analysis', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1']);
|
||
|
});
|
||
|
|
||
|
it('should have updated the layer', function () {
|
||
|
expect(this.layerDefModel.get('source')).toEqual('a1');
|
||
|
expect(this.layerDefModel.get('cartocss')).toEqual(jasmine.any(String));
|
||
|
expect(this.layerDefModel.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should return a new node', function () {
|
||
|
expect(this.nodeDefModel).toBeDefined();
|
||
|
expect(this.nodeDefModel.id).toEqual('a1');
|
||
|
});
|
||
|
|
||
|
it('should try to reset styles', function () {
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given a layer that already have an analysis for the source of given attrs', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'layerA',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
source: 'a0'
|
||
|
}
|
||
|
});
|
||
|
spyOn(this.layerDefModel, 'save').and.callThrough();
|
||
|
this.analysisDefinitionsCollection.add({ analysis_definition: { id: 'a0' } });
|
||
|
|
||
|
var nodeAttrs = {
|
||
|
id: 'a1',
|
||
|
type: 'trade-area',
|
||
|
source: 'a0',
|
||
|
kind: 'walk',
|
||
|
time: 123
|
||
|
};
|
||
|
|
||
|
this.nodeDefModel = this.userActions.createAnalysisNode(nodeAttrs, this.layerDefModel);
|
||
|
});
|
||
|
|
||
|
it('should not create a new analysis', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1']);
|
||
|
});
|
||
|
|
||
|
it('should updated layer', function () {
|
||
|
expect(this.layerDefModel.get('source')).toEqual('a1');
|
||
|
expect(this.layerDefModel.get('cartocss')).toEqual(jasmine.any(String));
|
||
|
expect(this.layerDefModel.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should return a new node', function () {
|
||
|
expect(this.nodeDefModel).toBeDefined();
|
||
|
expect(this.nodeDefModel.id).toEqual('a1');
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.saveAnalysis', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'l1',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
source: 'a0'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.analysisDefinitionNodesCollection.add({
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: {
|
||
|
query: 'SELECT * from alice'
|
||
|
}
|
||
|
});
|
||
|
this.aFormModel = new AnalysisFormModel({
|
||
|
id: 'a1',
|
||
|
type: 'buffer',
|
||
|
radius: 100,
|
||
|
source: 'a0'
|
||
|
}, {
|
||
|
analyses: analyses,
|
||
|
configModel: {},
|
||
|
layerDefinitionModel: this.layerDefModel,
|
||
|
analysisSourceOptionsModel: {}
|
||
|
});
|
||
|
spyOn(this.userActions, '_resetStylePerNode');
|
||
|
spyOn(this.aFormModel, 'isValid');
|
||
|
});
|
||
|
|
||
|
xit('should do nothing if invalid', function () {
|
||
|
this.aFormModel.isValid.and.returnValue(false);
|
||
|
this.userActions.saveAnalysis(this.aFormModel);
|
||
|
// TOFIX: no expect !?
|
||
|
});
|
||
|
|
||
|
describe('when valid', function () {
|
||
|
beforeEach(function () {
|
||
|
this.aFormModel.isValid.and.returnValue(true);
|
||
|
});
|
||
|
|
||
|
describe('when node does not exist for given form-model', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.aFormModel, 'createNodeDefinition').and.callThrough();
|
||
|
this.userActions.saveAnalysis(this.aFormModel);
|
||
|
});
|
||
|
|
||
|
it('should delegate back to form model to create node', function () {
|
||
|
expect(this.aFormModel.createNodeDefinition).toHaveBeenCalledWith(this.userActions);
|
||
|
});
|
||
|
|
||
|
it('should persist the new analysis', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1']);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when node-definition exists for given form-model', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layerDefModel.set('source', 'a1');
|
||
|
this.layerAnalysis = this.analysisDefinitionsCollection.add({
|
||
|
id: 'for-A',
|
||
|
analysis_definition: {
|
||
|
id: 'a1',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: { query: '' }
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
spyOn(this.layerAnalysis, 'save').and.callThrough();
|
||
|
spyOn(this.aFormModel, 'updateNodeDefinition').and.callThrough();
|
||
|
this.a1 = this.analysisDefinitionNodesCollection.get('a1');
|
||
|
|
||
|
this.userActions.saveAnalysis(this.aFormModel);
|
||
|
});
|
||
|
|
||
|
it('should delegate back to form model to update the node-definition', function () {
|
||
|
expect(this.aFormModel.updateNodeDefinition).toHaveBeenCalledWith(this.a1);
|
||
|
});
|
||
|
|
||
|
it('should persist the analysis change', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1']);
|
||
|
expect(this.layerAnalysis.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should set the USER_SAVED flag', function () {
|
||
|
var node = this.analysisDefinitionNodesCollection.get('a1');
|
||
|
expect(node.USER_SAVED).toBeTruthy();
|
||
|
});
|
||
|
|
||
|
it('should try to reset style if possible', function () {
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalled();
|
||
|
expect(this.userActions._resetStylePerNode.calls.argsFor(0)[3]).toBeTruthy();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.saveAnalysisSourceQuery', function () {
|
||
|
beforeEach(function () {
|
||
|
this.query = 'SELECT * FROM table';
|
||
|
this.a0 = this.analysisDefinitionNodesCollection.add({
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: { query: '' }
|
||
|
});
|
||
|
this.a1 = this.analysisDefinitionNodesCollection.add({
|
||
|
id: 'a1',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
source: { id: 'a0' }
|
||
|
}
|
||
|
});
|
||
|
this.layerDefModel = new LayerDefinitionModel({
|
||
|
id: 'layerA',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
source: 'a0',
|
||
|
table_name: 'table_name'
|
||
|
}
|
||
|
}, {
|
||
|
configModel: this.configModel,
|
||
|
parse: true
|
||
|
});
|
||
|
|
||
|
this.layerDefinitionsCollection.add(this.layerDefModel);
|
||
|
});
|
||
|
|
||
|
it('should not accept a non-source', function () {
|
||
|
expect(function () {
|
||
|
this.userActions.saveAnalysisSourceQuery(this.query, this.a1, this.layerDefModel);
|
||
|
}.bind(this)).toThrowError(/source/);
|
||
|
});
|
||
|
|
||
|
describe('setDefaultPropertiesByType', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.layerDefModel.styleModel, 'setDefaultPropertiesByType');
|
||
|
this.queryGeometryModel = this.a0.queryGeometryModel;
|
||
|
this.queryGeometryModel.set({
|
||
|
simple_geom: 'point',
|
||
|
query: 'SELECT * FROM table_name'
|
||
|
});
|
||
|
spyOn(this.userActions, '_resetStylePerNode');
|
||
|
this.userActions.saveAnalysisSourceQuery(this.query, this.a0, this.layerDefModel);
|
||
|
});
|
||
|
|
||
|
it('should try to reset style if possible', function () {
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when there is no analysis', function () {
|
||
|
/*
|
||
|
There is no possibility to have a layer without an analysis
|
||
|
because source analysis are pre generated in the backend
|
||
|
*/
|
||
|
});
|
||
|
|
||
|
describe('when there is an persisted analysis already', function () {
|
||
|
beforeEach(function () {
|
||
|
this.analysis = this.analysisDefinitionsCollection.add({
|
||
|
id: 'for-layerA',
|
||
|
analysis_definition: { id: 'a1' }
|
||
|
});
|
||
|
spyOn(this.analysis, 'save');
|
||
|
this.userActions.saveAnalysisSourceQuery(this.query, this.a0, this.layerDefModel);
|
||
|
});
|
||
|
|
||
|
it('should set query on analysis-definition-node-model', function () {
|
||
|
var query = this.a0.get('query');
|
||
|
expect(query).toEqual(this.query);
|
||
|
expect(_.isEmpty(query)).toBe(false);
|
||
|
});
|
||
|
|
||
|
it('should save the analysis that contains the affected node', function () {
|
||
|
expect(this.analysis.save).toHaveBeenCalled();
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1']);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.saveWidgetOption', function () {
|
||
|
beforeEach(function () {
|
||
|
this.a0 = this.analysisDefinitionNodesCollection.add({
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: { query: '' }
|
||
|
});
|
||
|
this.a1 = this.analysisDefinitionNodesCollection.add({
|
||
|
id: 'a1',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
source: { id: 'a0' }
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'layerA',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
source: 'a1'
|
||
|
}
|
||
|
});
|
||
|
spyOn(this.layerDefModel, 'save').and.callThrough();
|
||
|
|
||
|
this.widgetOptionModel = new WidgetOptionModel({
|
||
|
type: 'category'
|
||
|
});
|
||
|
spyOn(this.widgetOptionModel, 'analysisDefinitionNodeModel').and.returnValue(this.a1);
|
||
|
spyOn(this.widgetOptionModel, 'layerDefinitionModel').and.returnValue(this.layerDefModel);
|
||
|
spyOn(this.widgetOptionModel, 'save');
|
||
|
});
|
||
|
|
||
|
describe('when source of widget is not yet persisted', function () {
|
||
|
beforeEach(function () {
|
||
|
this.userActions.saveWidgetOption(this.widgetOptionModel);
|
||
|
});
|
||
|
|
||
|
it('should create a new analysis', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1']);
|
||
|
});
|
||
|
|
||
|
it('should not persist layer, it is necessary', function () {
|
||
|
expect(this.layerDefModel.save).not.toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should delegate side-effects to the option model', function () {
|
||
|
expect(this.widgetOptionModel.save).toHaveBeenCalledWith(this.widgetDefinitionsCollection);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when source of widget is already persisted', function () {
|
||
|
beforeEach(function () {
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: 'for-A',
|
||
|
analysis_definition: { id: 'a1' }
|
||
|
});
|
||
|
this.userActions.saveWidgetOption(this.widgetOptionModel);
|
||
|
});
|
||
|
|
||
|
it('should only delegate side-effects to the option model', function () {
|
||
|
expect(this.widgetOptionModel.save).toHaveBeenCalledWith(this.widgetDefinitionsCollection);
|
||
|
});
|
||
|
|
||
|
it('should not save associated layer-definition-model, it is not necessary', function () {
|
||
|
expect(this.layerDefModel.save).not.toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when there is no analysis-definition-node-model available (e.g. time-series none-option)', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.analysisDefinitionsCollection, 'saveAnalysisForLayer');
|
||
|
this.widgetOptionModel.analysisDefinitionNodeModel.and.returnValue(undefined);
|
||
|
this.userActions.saveWidgetOption(this.widgetOptionModel);
|
||
|
});
|
||
|
|
||
|
it('should only delegate side-effects to the option model', function () {
|
||
|
expect(this.widgetOptionModel.save).toHaveBeenCalledWith(this.widgetDefinitionsCollection);
|
||
|
|
||
|
expect(this.analysisDefinitionsCollection.saveAnalysisForLayer).not.toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.deleteAnalysisNode', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.userActions, '_resetStylePerNode');
|
||
|
});
|
||
|
|
||
|
describe('when there is a layer with some analysis', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layerA = this.layerDefinitionsCollection.add({
|
||
|
id: 'A',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
source: 'a2',
|
||
|
table_name: 'alice'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: 'for-layer-A',
|
||
|
analysis_definition: {
|
||
|
id: 'a2',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'a1',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: {
|
||
|
query: 'SELECT * FROM alice'
|
||
|
},
|
||
|
options: {
|
||
|
style_history: {
|
||
|
A: {
|
||
|
options: {
|
||
|
tile_style: 'wadus',
|
||
|
style_properties: {
|
||
|
type: 'wadus'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('deleting a1', function () {
|
||
|
beforeEach(function () {
|
||
|
this.userActions.deleteAnalysisNode('a1');
|
||
|
});
|
||
|
|
||
|
it('should delete self and dependent nodes', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).toEqual(['a0'], 'should leave a0 at least');
|
||
|
});
|
||
|
|
||
|
it('should update analysis to point to source of deleted node', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a0']);
|
||
|
});
|
||
|
|
||
|
it('should update the layer to point to the primary source', function () {
|
||
|
expect(this.layerA.get('source')).toEqual('a0');
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['A']);
|
||
|
});
|
||
|
|
||
|
it('should reset styles', function () {
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should copy styles from analysis node', function () {
|
||
|
expect(this.layerA.styleModel.get('type')).toEqual('wadus');
|
||
|
expect(this.layerA.get('cartocss')).toEqual('wadus');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('deleting a2', function () {
|
||
|
beforeEach(function () {
|
||
|
this.userActions.deleteAnalysisNode('a2');
|
||
|
});
|
||
|
|
||
|
it('should reset styles', function () {
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('for some interrelated layers', function () {
|
||
|
beforeEach(function () {
|
||
|
// creates a nodes graph to test various scenarios:
|
||
|
// a0 <-- head of layer A
|
||
|
// c1 <-- head of layer C + widget
|
||
|
// b2
|
||
|
// b1 <-- head layer B
|
||
|
// b0 <-- widget
|
||
|
// c0
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: 'layer-A',
|
||
|
analysis_definition: {
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: {
|
||
|
query: 'SELECT * FROM a_single_source'
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: 'layer-B',
|
||
|
analysis_definition: {
|
||
|
id: 'b2',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'b1',
|
||
|
type: 'trade-area',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'b0',
|
||
|
type: 'source',
|
||
|
params: {
|
||
|
query: 'SELECT * FROM bar'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: 'layer-C',
|
||
|
analysis_definition: {
|
||
|
id: 'c1',
|
||
|
type: 'intersection',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'c0',
|
||
|
type: 'source',
|
||
|
params: {
|
||
|
query: 'SELECT * FROM my_polygons'
|
||
|
}
|
||
|
},
|
||
|
target: { id: 'b2' }
|
||
|
},
|
||
|
options: {
|
||
|
primary_source_name: 'source'
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.layerDefinitionsCollection.add([
|
||
|
{
|
||
|
id: 'A',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
source: 'a0'
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
id: 'B',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'b',
|
||
|
source: 'b2'
|
||
|
}
|
||
|
}, {
|
||
|
id: 'C',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'c',
|
||
|
source: 'c1'
|
||
|
}
|
||
|
}
|
||
|
]);
|
||
|
|
||
|
this.widgetDefinitionsCollection.add([
|
||
|
{
|
||
|
id: 'for-b0',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'b0'
|
||
|
}
|
||
|
}, {
|
||
|
id: 'for-c1',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'c1'
|
||
|
}
|
||
|
}
|
||
|
]);
|
||
|
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).toEqual(['a0', 'b0', 'b1', 'b2', 'c0', 'c1'], 'should have created individual nodes');
|
||
|
});
|
||
|
|
||
|
it('should do nothing if the node does not exist', function () {
|
||
|
expect(this.userActions.deleteAnalysisNode('x1')).toBe(false);
|
||
|
expect(this.userActions.deleteAnalysisNode('')).toBe(false);
|
||
|
expect(this.userActions.deleteAnalysisNode(undefined)).toBe(false);
|
||
|
expect(this.userActions.deleteAnalysisNode(null)).toBe(false);
|
||
|
expect(this.userActions.deleteAnalysisNode(true)).toBe(false);
|
||
|
});
|
||
|
|
||
|
describe('when given a head node w/o any dependent nodes (c1)', function () {
|
||
|
beforeEach(function () {
|
||
|
this.c1 = this.analysisDefinitionNodesCollection.get('c1');
|
||
|
this.C = this.layerDefinitionsCollection.get('C');
|
||
|
|
||
|
spyOn(this.c1, 'destroy').and.callThrough();
|
||
|
spyOn(this.C, 'save').and.callThrough();
|
||
|
spyOn(this.analysisDefinitionsCollection, 'create').and.callThrough();
|
||
|
|
||
|
this.userActions.deleteAnalysisNode('c1');
|
||
|
});
|
||
|
|
||
|
it('should delete dependent nodes', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).not.toContain('c1');
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).toEqual(['a0', 'b0', 'b1', 'b2', 'c0'], 'c1 and its primary source c0 should have been removed');
|
||
|
});
|
||
|
|
||
|
it('should update affected analysis', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a0', 'b2', 'c0'], 'should have updated c1 => c0');
|
||
|
});
|
||
|
|
||
|
it('should update layer to point to new head', function () {
|
||
|
expect(this.C.save).toHaveBeenCalled();
|
||
|
expect(this.C.get('source')).toEqual('c0');
|
||
|
expect(this.layerDefinitionsCollection.pluck('source')).toEqual(['a0', 'b2', 'c0']);
|
||
|
});
|
||
|
|
||
|
it('should delete dependent widgets', function () {
|
||
|
expect(this.widgetDefinitionsCollection.pluck('source')).toEqual(['b0']);
|
||
|
});
|
||
|
|
||
|
it('should delete node', function () {
|
||
|
expect(this.c1.destroy).toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given a head node which have dependent nodes (b2)', function () {
|
||
|
beforeEach(function () {
|
||
|
this.b2 = this.analysisDefinitionNodesCollection.get('b2');
|
||
|
this.B = this.layerDefinitionsCollection.get('B');
|
||
|
|
||
|
spyOn(this.b2, 'destroy').and.callThrough();
|
||
|
spyOn(this.B, 'save').and.callThrough();
|
||
|
|
||
|
this.userActions.deleteAnalysisNode('b2');
|
||
|
});
|
||
|
|
||
|
it('should delete dependent nodes', function () {
|
||
|
var nodeIds = this.analysisDefinitionNodesCollection.pluck('id');
|
||
|
expect(nodeIds).not.toContain('b2');
|
||
|
expect(nodeIds).not.toContain('c1');
|
||
|
expect(nodeIds).not.toContain('c0');
|
||
|
expect(nodeIds).toEqual(['a0', 'b0', 'b1']);
|
||
|
});
|
||
|
|
||
|
it('should delete analysis for C', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).not.toContain('c1');
|
||
|
});
|
||
|
|
||
|
it('should have created a new analysis for the remaining b0', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toContain('b1');
|
||
|
});
|
||
|
|
||
|
it('should update layer B to point to new head', function () {
|
||
|
expect(this.B.save).toHaveBeenCalled();
|
||
|
expect(this.B.get('source')).toEqual('b1');
|
||
|
});
|
||
|
|
||
|
it('should delete C layer', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['A', 'B']);
|
||
|
});
|
||
|
|
||
|
it('should not affect widgets', function () {
|
||
|
expect(this.widgetDefinitionsCollection.pluck('source')).toEqual(['b0']);
|
||
|
});
|
||
|
|
||
|
it('should delete node', function () {
|
||
|
expect(this.b2.destroy).toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given a node which is neither a head or source (b1)', function () {
|
||
|
beforeEach(function () {
|
||
|
this.b1 = this.analysisDefinitionNodesCollection.get('b1');
|
||
|
|
||
|
spyOn(this.b1, 'destroy').and.callThrough();
|
||
|
|
||
|
this.userActions.deleteAnalysisNode('b1');
|
||
|
});
|
||
|
|
||
|
it('should delete dependent nodes', function () {
|
||
|
var nodeIds = this.analysisDefinitionNodesCollection.pluck('id');
|
||
|
expect(nodeIds).not.toContain('b2');
|
||
|
expect(nodeIds).not.toContain('b1');
|
||
|
expect(nodeIds).not.toContain('c1');
|
||
|
expect(nodeIds).toEqual(['a0', 'b0']);
|
||
|
});
|
||
|
|
||
|
it('should have created a new analysis for the remaining b0', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toContain('b0');
|
||
|
});
|
||
|
|
||
|
it('should delete analysis for C', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).not.toContain('c1');
|
||
|
});
|
||
|
|
||
|
it('should delete affected layers', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['A', 'B'], 'only C');
|
||
|
});
|
||
|
|
||
|
it('should not affect widgets', function () {
|
||
|
expect(this.widgetDefinitionsCollection.pluck('source')).toEqual(['b0']);
|
||
|
});
|
||
|
|
||
|
it('should delete node', function () {
|
||
|
expect(this.b1.destroy).toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.createLayerFromTable', function () {
|
||
|
beforeEach(function () {
|
||
|
interceptAjaxCall = function (params) {
|
||
|
if (/layers/.test(params.url)) {
|
||
|
params.success && params.success({
|
||
|
id: undefined,
|
||
|
options: {},
|
||
|
order: 2,
|
||
|
infowindow: '',
|
||
|
tooltip: '',
|
||
|
kind: 'carto'
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
});
|
||
|
|
||
|
it('should create layer with infowindows and tooltips', function () {
|
||
|
var tableModel = new TableModel({ name: 'tableName' }, { configModel: this.configModel });
|
||
|
var newLayer = this.userActions.createLayerFromTable(tableModel);
|
||
|
expect(newLayer.get('infowindow')).toBeDefined();
|
||
|
expect(newLayer.get('tooltip')).toBeDefined();
|
||
|
});
|
||
|
|
||
|
it('should create a new analysis for the source', function () {
|
||
|
var tableModel = new TableModel({ name: 'foobar' }, { configModel: this.configModel });
|
||
|
var newLayer = this.userActions.createLayerFromTable(tableModel);
|
||
|
var analysisDefModel = this.analysisDefinitionsCollection.findWhere({ node_id: newLayer.get('source') });
|
||
|
expect(analysisDefModel).toBeDefined();
|
||
|
|
||
|
var nodeDefModel = this.analysisDefinitionNodesCollection.get(newLayer.get('source'));
|
||
|
expect(nodeDefModel).toBeDefined();
|
||
|
expect(nodeDefModel.attributes).toEqual({
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
table_name: 'foobar',
|
||
|
query: 'SELECT * FROM foobar',
|
||
|
status: 'ready'
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should create a new analysis for the source quoting table names if needed', function () {
|
||
|
var tableModel = new TableModel({ name: '000cd294-b124-4f82-b569-0f7fe41d2db8' }, { configModel: this.configModel });
|
||
|
var newLayer = this.userActions.createLayerFromTable(tableModel);
|
||
|
var analysisDefModel = this.analysisDefinitionsCollection.findWhere({ node_id: newLayer.get('source') });
|
||
|
expect(analysisDefModel).toBeDefined();
|
||
|
|
||
|
var nodeDefModel = this.analysisDefinitionNodesCollection.get(newLayer.get('source'));
|
||
|
expect(nodeDefModel).toBeDefined();
|
||
|
expect(nodeDefModel.attributes).toEqual({
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
table_name: '"000cd294-b124-4f82-b569-0f7fe41d2db8"',
|
||
|
query: 'SELECT * FROM "000cd294-b124-4f82-b569-0f7fe41d2db8"',
|
||
|
status: 'ready'
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should create a new analysis for the source getting owner name if present', function () {
|
||
|
var tableModel = new TableModel({ name: '"user".table' }, { configModel: this.configModel });
|
||
|
this.configModel.set('user_name', 'pepito');
|
||
|
var newLayer = this.userActions.createLayerFromTable(tableModel);
|
||
|
var analysisDefModel = this.analysisDefinitionsCollection.findWhere({ node_id: newLayer.get('source') });
|
||
|
expect(analysisDefModel).toBeDefined();
|
||
|
|
||
|
expect(newLayer.get('table_name')).toBe('table');
|
||
|
expect(newLayer.get('user_name')).toBe('user');
|
||
|
|
||
|
var nodeDefModel = this.analysisDefinitionNodesCollection.get(newLayer.get('source'));
|
||
|
expect(nodeDefModel).toBeDefined();
|
||
|
expect(nodeDefModel.attributes).toEqual({
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
table_name: 'user.table',
|
||
|
query: 'SELECT * FROM user.table',
|
||
|
status: 'ready'
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should create a new analysis for the source passing the table model', function () {
|
||
|
var tableModel = new TableModel({ name: 'foobar', privacy: 'PRIVATE' }, { configModel: this.configModel });
|
||
|
var newLayer = this.userActions.createLayerFromTable(tableModel);
|
||
|
var analysisDefModel = this.analysisDefinitionsCollection.findWhere({ node_id: newLayer.get('source') });
|
||
|
expect(analysisDefModel).toBeDefined();
|
||
|
|
||
|
var nodeDefModel = this.analysisDefinitionNodesCollection.get(newLayer.get('source'));
|
||
|
expect(nodeDefModel.tableModel).toBeDefined();
|
||
|
expect(nodeDefModel.tableModel.get('name')).toEqual('foobar');
|
||
|
expect(nodeDefModel.tableModel.get('privacy')).toEqual('PRIVATE');
|
||
|
});
|
||
|
|
||
|
it('should invoke the success callback if layer is correctly created', function () {
|
||
|
spyOn(this.layerDefinitionsCollection, 'create');
|
||
|
var successCallback = jasmine.createSpy('successCallback');
|
||
|
var tableModel = new TableModel({ name: 'tableName' }, { configModel: this.configModel });
|
||
|
this.userActions.createLayerFromTable(tableModel, { success: successCallback });
|
||
|
this.layerDefinitionsCollection.create.calls.argsFor(0)[1].success();
|
||
|
expect(successCallback).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should add style_properties if geometry is provided', function () {
|
||
|
var tableModel = new TableModel({ name: 'tableName', geometry_types: ['st_multilinestring'] }, { configModel: this.configModel });
|
||
|
var layer = this.userActions.createLayerFromTable(tableModel);
|
||
|
expect(layer.styleModel.get('type')).toBe('simple');
|
||
|
var stroke = layer.styleModel.get('stroke');
|
||
|
expect(stroke.color.fixed).toBe('#4CC8A3');
|
||
|
|
||
|
tableModel.set({
|
||
|
name: 'tableName',
|
||
|
geometry_types: ['st_polygon']
|
||
|
});
|
||
|
var layer2 = this.userActions.createLayerFromTable(tableModel);
|
||
|
expect(layer2.styleModel.get('type')).toBe('simple');
|
||
|
var fill = layer2.styleModel.get('fill');
|
||
|
expect(fill.color.fixed).toBe('#826DBA');
|
||
|
});
|
||
|
|
||
|
it('should invoke the error callback if layer creation fails', function () {
|
||
|
spyOn(this.layerDefinitionsCollection, 'create');
|
||
|
var errorCallback = jasmine.createSpy('errorCallback');
|
||
|
var tableModel = new TableModel({ name: 'tableName' }, { configModel: this.configModel });
|
||
|
this.userActions.createLayerFromTable(tableModel, { error: errorCallback });
|
||
|
this.layerDefinitionsCollection.create.calls.argsFor(0)[1].error();
|
||
|
expect(errorCallback).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should place the layer below the "Tiled" layer on top', function () {
|
||
|
this.layerDefinitionsCollection.add({
|
||
|
id: 'labels-on-top',
|
||
|
kind: 'tiled',
|
||
|
order: 1
|
||
|
});
|
||
|
this.layerDefinitionsCollection.add({
|
||
|
id: 'cartodb-layer',
|
||
|
kind: 'carto',
|
||
|
options: { table_name: 'carto' },
|
||
|
order: 0
|
||
|
});
|
||
|
|
||
|
spyOn(this.layerDefinitionsCollection, 'create').and.callThrough();
|
||
|
spyOn(this.layerDefinitionsCollection, 'save').and.callThrough();
|
||
|
|
||
|
var tableModel = new TableModel({ name: 'tableName' }, { configModel: this.configModel });
|
||
|
this.userActions.createLayerFromTable(tableModel);
|
||
|
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['cartodb-layer', undefined, 'labels-on-top']);
|
||
|
expect(this.layerDefinitionsCollection.pluck('order')).toEqual([0, 1, 2]);
|
||
|
|
||
|
// Layer is created successfully
|
||
|
this.layerDefinitionsCollection.create.calls.argsFor(0)[1].success();
|
||
|
|
||
|
expect(this.layerDefinitionsCollection.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should place the layer below the "Torque" layer on top', function () {
|
||
|
this.layerDefinitionsCollection.add({
|
||
|
id: 'torque-layer',
|
||
|
kind: 'torque',
|
||
|
options: { table_name: 'torque' },
|
||
|
order: 1
|
||
|
});
|
||
|
this.layerDefinitionsCollection.add({
|
||
|
id: 'cartodb-layer',
|
||
|
kind: 'carto',
|
||
|
options: { table_name: 'carto' },
|
||
|
order: 0
|
||
|
});
|
||
|
|
||
|
spyOn(this.layerDefinitionsCollection, 'create').and.callThrough();
|
||
|
spyOn(this.layerDefinitionsCollection, 'save').and.callThrough();
|
||
|
var tableModel = new TableModel({ name: 'tableName' }, { configModel: this.configModel });
|
||
|
this.userActions.createLayerFromTable(tableModel);
|
||
|
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['cartodb-layer', undefined, 'torque-layer']);
|
||
|
expect(this.layerDefinitionsCollection.pluck('order')).toEqual([0, 1, 2]);
|
||
|
|
||
|
// Layer is created successfully
|
||
|
this.layerDefinitionsCollection.create.calls.argsFor(0)[1].success();
|
||
|
expect(this.layerDefinitionsCollection.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should place the new layer above the "CartoDB" layer on top', function () {
|
||
|
this.layerDefinitionsCollection.add({
|
||
|
id: 'layer-0',
|
||
|
kind: 'carto',
|
||
|
options: { table_name: 'alice' },
|
||
|
order: 0
|
||
|
});
|
||
|
this.layerDefinitionsCollection.add({
|
||
|
id: 'layer-1',
|
||
|
kind: 'carto',
|
||
|
options: { table_name: 'bob' },
|
||
|
order: 1
|
||
|
});
|
||
|
|
||
|
spyOn(this.layerDefinitionsCollection, 'create').and.callThrough();
|
||
|
spyOn(this.layerDefinitionsCollection, 'save').and.callThrough();
|
||
|
|
||
|
var tableModel = new TableModel({ name: 'tableName' }, { configModel: this.configModel });
|
||
|
var newLayer = this.userActions.createLayerFromTable(tableModel);
|
||
|
newLayer.set('id', 'new-layer');
|
||
|
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['layer-0', 'layer-1', 'new-layer']);
|
||
|
expect(this.layerDefinitionsCollection.pluck('order')).toEqual([0, 1, 2]);
|
||
|
|
||
|
// Layer is created successfully
|
||
|
this.layerDefinitionsCollection.create.calls.argsFor(0)[1].success();
|
||
|
|
||
|
// Order of other layers has not been changed
|
||
|
expect(this.layerDefinitionsCollection.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should create analysis for all layers that lack one', function () {
|
||
|
this.layerDefinitionsCollection.add({ kind: 'tiled' });
|
||
|
|
||
|
// Layers w/ analysis
|
||
|
this.layerA = this.layerDefinitionsCollection.add({
|
||
|
id: 'layerA',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
source: 'a0'
|
||
|
}
|
||
|
});
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
analysis_definition: {
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: {}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Layer w/o analysis
|
||
|
this.layerB = this.layerDefinitionsCollection.add({
|
||
|
id: 'layerB',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'b',
|
||
|
source: 'b0'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
spyOn(this.analysisDefinitionsCollection, 'saveAnalysisForLayer');
|
||
|
|
||
|
var tableModel = new TableModel({ name: 'tableName' }, { configModel: this.configModel });
|
||
|
this.userActions.createLayerFromTable(tableModel);
|
||
|
|
||
|
// Should create an aalysis for layerB
|
||
|
expect(this.analysisDefinitionsCollection.saveAnalysisForLayer).toHaveBeenCalled();
|
||
|
expect(this.analysisDefinitionsCollection.saveAnalysisForLayer.calls.count()).toEqual(2);
|
||
|
expect(this.analysisDefinitionsCollection.saveAnalysisForLayer.calls.argsFor(0)[0].id).toEqual('layerB');
|
||
|
});
|
||
|
|
||
|
it('should place new layer below labels on top and torque if they exists', function () {
|
||
|
this.layerDefinitionsCollection.add({
|
||
|
id: 'labels-on-top',
|
||
|
kind: 'tiled',
|
||
|
order: 1
|
||
|
});
|
||
|
this.layerDefinitionsCollection.add({
|
||
|
id: 'torque-layer',
|
||
|
kind: 'torque',
|
||
|
options: { table_name: 'torque' },
|
||
|
order: 0
|
||
|
});
|
||
|
|
||
|
spyOn(this.layerDefinitionsCollection, 'create').and.callThrough();
|
||
|
spyOn(this.layerDefinitionsCollection, 'save').and.callThrough();
|
||
|
var tableModel = new TableModel({ name: 'tableName' }, { configModel: this.configModel });
|
||
|
this.userActions.createLayerFromTable(tableModel);
|
||
|
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual([undefined, 'torque-layer', 'labels-on-top']);
|
||
|
expect(this.layerDefinitionsCollection.pluck('order')).toEqual([0, 1, 2]);
|
||
|
|
||
|
// Layer is created successfully
|
||
|
this.layerDefinitionsCollection.create.calls.argsFor(0)[1].success();
|
||
|
expect(this.layerDefinitionsCollection.save).toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.createLayerForAnalysisNode', function () {
|
||
|
var userActions;
|
||
|
|
||
|
beforeEach(function () {
|
||
|
userActions = this.userActions;
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: '1st',
|
||
|
analysis_definition: {
|
||
|
id: 'a2',
|
||
|
type: 'trade-area',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'a1',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: {
|
||
|
query: 'SELECT * FROM something'
|
||
|
},
|
||
|
options: {
|
||
|
table_name: 'something'
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
options: {
|
||
|
style_history: {
|
||
|
layer_with_styles: {
|
||
|
options: {
|
||
|
tile_style: 'wadus',
|
||
|
style_properties: {
|
||
|
type: 'wadus'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.widgetDefinitionsCollection.add({
|
||
|
id: 'should-not-change',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'w101'
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should not allow to create layer below or on basemap', function () {
|
||
|
expect(function () { userActions.createLayerForAnalysisNode('a1', 'a'); }).toThrowError(/required/);
|
||
|
expect(function () { userActions.createLayerForAnalysisNode('a1', null, null); }).toThrowError(/required/);
|
||
|
|
||
|
expect(function () { userActions.createLayerForAnalysisNode('a1', 'a', {}); }).toThrowError(/base layer/);
|
||
|
expect(function () { userActions.createLayerForAnalysisNode('a1', 'a', { at: 0 }); }).toThrowError(/base layer/);
|
||
|
expect(function () { userActions.createLayerForAnalysisNode('a1', 'a', { at: null }); }).toThrowError(/base layer/);
|
||
|
});
|
||
|
|
||
|
it('should throw an error if given node does not exist', function () {
|
||
|
expect(function () { userActions.createLayerForAnalysisNode('x1', 'x', { at: 0 }); }).toThrowError(/does not exist/);
|
||
|
});
|
||
|
|
||
|
it('should throw an error if max layers limit is reached', function () {
|
||
|
var error;
|
||
|
spyOn(this.layerDefinitionsCollection, 'getNumberOfDataLayers').and.returnValue(4);
|
||
|
try {
|
||
|
userActions.createLayerForAnalysisNode('x1', 'x', { at: 0 });
|
||
|
} catch (err) {
|
||
|
error = err;
|
||
|
}
|
||
|
expect(error).toBeDefined();
|
||
|
expect(error.message).toContain('max');
|
||
|
expect(error.userMaxLayers).toEqual(4);
|
||
|
});
|
||
|
|
||
|
describe('when given node is a source node', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layerDefinitionsCollection.add([
|
||
|
{
|
||
|
id: 'basemap',
|
||
|
kind: 'tiled',
|
||
|
order: 0
|
||
|
}, {
|
||
|
id: 'layerA',
|
||
|
order: 1,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
source: 'a0',
|
||
|
table_name: 'alice',
|
||
|
table_name_alias: 'alice_alias',
|
||
|
cartocss: 'wadus',
|
||
|
style_properties: {
|
||
|
type: 'wadus'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
]);
|
||
|
|
||
|
spyOn(this.layerDefinitionsCollection, 'create').and.callThrough();
|
||
|
spyOn(this.userActions, '_resetStylePerNode');
|
||
|
this.userActions.createLayerForAnalysisNode('a0', 'a', { at: 1 });
|
||
|
this.newLayerDefModel = this.layerDefinitionsCollection.findWhere({ letter: 'b' });
|
||
|
});
|
||
|
|
||
|
it('should create a new layer', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('letter')).toEqual([undefined, 'b', 'a'], 'should add new layer at the given at position');
|
||
|
expect(this.newLayerDefModel.get('letter')).toEqual('b', 'should have new letter');
|
||
|
expect(this.newLayerDefModel.get('source')).toEqual('a0', 'should have same source');
|
||
|
expect(this.newLayerDefModel.get('sql')).toEqual('SELECT * FROM alice', 'should have same SQL');
|
||
|
expect(this.newLayerDefModel.get('table_name')).toEqual('alice', 'should have same table name');
|
||
|
expect(this.newLayerDefModel.get('table_name_alias')).toEqual('alice_alias', 'should have same table name alias');
|
||
|
expect(this.newLayerDefModel.get('order')).toEqual(1, 'should be the same as given at position');
|
||
|
expect(this.newLayerDefModel.get('cartocss')).toEqual('wadus', 'should have same cartocss');
|
||
|
expect(this.newLayerDefModel.styleModel.get('type')).toEqual('wadus', 'should have same style properties');
|
||
|
});
|
||
|
|
||
|
it('should not force-reset styles, they are copied from original layer', function () {
|
||
|
this.userActions._resetStylePerNode.calls.reset();
|
||
|
this.layerDefinitionsCollection.create.calls.argsFor(0)[1].success();
|
||
|
var queryGeometryModel = this.newLayerDefModel.getAnalysisDefinitionNodeModel().queryGeometryModel;
|
||
|
queryGeometryModel.set({ status: 'fetched', ready: true, simple_geom: 'line' });
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalled();
|
||
|
expect(this.userActions._resetStylePerNode.calls.argsFor(0)[3]).toBeFalsy();
|
||
|
});
|
||
|
|
||
|
it('should not change any analysis', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).toEqual(['a0']);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Node is NOT a head of a node, e.g. given nodeId is 'a2' this would create a new layer B which takes over the
|
||
|
// ownership of the given node and its underlying nodes
|
||
|
// _______ _______ ______
|
||
|
// | A | | A | | B |
|
||
|
// | | | | | |
|
||
|
// | [A3] | | [A3] | | {B2} |
|
||
|
// | {A2} | => | {B2} | | [B1] |
|
||
|
// | [A1] | | | | [B0] |
|
||
|
// | [A0] | | | | |
|
||
|
// |______| |______| |______|
|
||
|
describe('when given node is NOT a head of any layer', function () {
|
||
|
beforeEach(function () {
|
||
|
this.analysis = {
|
||
|
id: 'A4',
|
||
|
analysis_definition: {
|
||
|
id: 'a4',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
radius: 40,
|
||
|
source: {
|
||
|
id: 'a3',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
radius: 30,
|
||
|
source: this.analysisDefinitionNodesCollection.get('a2').toJSON()
|
||
|
},
|
||
|
options: {
|
||
|
style_history: {
|
||
|
l1: {
|
||
|
options: {
|
||
|
tile_style: 'wadus',
|
||
|
style_properties: {
|
||
|
type: 'wadus'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
this.analysisDefinitionsCollection.reset([this.analysis]);
|
||
|
this.A4 = this.analysisDefinitionsCollection.get('A4');
|
||
|
spyOn(this.A4, 'save').and.callThrough();
|
||
|
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'l1',
|
||
|
order: 1,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
table_name: 'foobar',
|
||
|
table_name_alias: 'alias',
|
||
|
cartocss: 'before',
|
||
|
source: 'a4'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.widgetDefinitionsCollection.add({
|
||
|
id: 'should-change',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'a2'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
expect(this.widgetDefinitionsCollection.size()).toBe(2);
|
||
|
});
|
||
|
|
||
|
it('should handle the requests in the correct order', function () {
|
||
|
var calls = [];
|
||
|
var newLayerModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'm1',
|
||
|
order: 1,
|
||
|
kind: 'carto',
|
||
|
type: 'CartoDB',
|
||
|
options: {
|
||
|
table_name: 'foobar',
|
||
|
table_name_alias: 'alias',
|
||
|
cartocss: 'before',
|
||
|
source: 'a4'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
spyOn(this.layerDefinitionsCollection, 'add').and.returnValue(newLayerModel);
|
||
|
|
||
|
newLayerModel.sync = function (action, model, opts) {
|
||
|
calls.push('layers');
|
||
|
var deferred = $.Deferred();
|
||
|
deferred.resolve(model.toJSON());
|
||
|
return deferred.promise();
|
||
|
};
|
||
|
|
||
|
_.each(this.analysisDefinitionsCollection.models, function (model) {
|
||
|
model.sync = function (action, model, opts) {
|
||
|
calls.push('analysis');
|
||
|
var deferredZ = $.Deferred();
|
||
|
deferredZ.resolve(model.toJSON());
|
||
|
return deferredZ.promise();
|
||
|
};
|
||
|
}, this);
|
||
|
|
||
|
this.userActions.createLayerForAnalysisNode('a2', 'a', { at: 2 });
|
||
|
expect(calls[0]).toEqual('analysis');
|
||
|
expect(calls[1]).toEqual('layers');
|
||
|
});
|
||
|
|
||
|
describe('and does not have saved styles', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.widgetDefinitionsCollection, 'create').and.callThrough();
|
||
|
this.userActions.createLayerForAnalysisNode('a2', 'a', { at: 2 });
|
||
|
});
|
||
|
|
||
|
describe('should create new layer with', function () {
|
||
|
beforeEach(function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('letter')).toEqual(['a', 'b']);
|
||
|
this.newLayerDefModel = this.layerDefinitionsCollection.last();
|
||
|
});
|
||
|
|
||
|
it('source pointing to new node', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.get('b2')).toBeDefined();
|
||
|
expect(this.newLayerDefModel.get('source')).toEqual('b2');
|
||
|
});
|
||
|
|
||
|
it('table name of prev layer', function () {
|
||
|
expect(this.newLayerDefModel.get('table_name')).toEqual('foobar');
|
||
|
});
|
||
|
|
||
|
it('should set table name alias of prev layer', function () {
|
||
|
expect(this.newLayerDefModel.get('table_name_alias')).toEqual('alias');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should remove all widgets pointing to the affected node', function () {
|
||
|
expect(this.widgetDefinitionsCollection.size()).toBe(1);
|
||
|
});
|
||
|
|
||
|
it('should still have same unaffected nodes on layer A', function () {
|
||
|
expect(this.layerDefModel.get('source')).toEqual('a4');
|
||
|
expect(this.analysisDefinitionNodesCollection.get('a4')).toBeDefined();
|
||
|
expect(this.analysisDefinitionNodesCollection.get('a3')).toBeDefined();
|
||
|
});
|
||
|
|
||
|
it('the head of the moved node should now point to the head of the new layer B', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.get('a3').get('source')).toEqual('b2');
|
||
|
});
|
||
|
|
||
|
it('should have created new node from the sub-tree of the given node', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.get('b2')).toBeDefined();
|
||
|
expect(this.analysisDefinitionNodesCollection.get('b1')).toBeDefined();
|
||
|
expect(this.analysisDefinitionNodesCollection.get('b0')).toBeDefined();
|
||
|
});
|
||
|
|
||
|
it('should have removed the underlying no-longer-used nodes', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.get('a2')).toBeUndefined();
|
||
|
expect(this.analysisDefinitionNodesCollection.get('a1')).toBeUndefined();
|
||
|
expect(this.analysisDefinitionNodesCollection.get('a0')).toBeUndefined();
|
||
|
});
|
||
|
|
||
|
it('should save the existing analysis', function () {
|
||
|
expect(this.A4.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should create a new analysis for new layer', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a4', 'b2']);
|
||
|
});
|
||
|
|
||
|
describe('when created a layer successfully', function () {
|
||
|
it('should recreate all affected widgets', function () {
|
||
|
expect(this.widgetDefinitionsCollection.create).toHaveBeenCalledWith(
|
||
|
jasmine.objectContaining({
|
||
|
type: 'formula',
|
||
|
options: jasmine.objectContaining({
|
||
|
sync_on_bbox_change: true
|
||
|
}),
|
||
|
avoidNotification: true,
|
||
|
source: jasmine.objectContaining({
|
||
|
id: 'b2'
|
||
|
}),
|
||
|
layer_id: undefined,
|
||
|
order: 0
|
||
|
}),
|
||
|
jasmine.objectContaining({
|
||
|
wait: true,
|
||
|
success: jasmine.any(Function),
|
||
|
error: jasmine.any(Function)
|
||
|
})
|
||
|
);
|
||
|
});
|
||
|
|
||
|
describe('when widgets are created', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.userActions, '_resetStylePerNode');
|
||
|
spyOn(this.layerDefinitionsCollection, 'save').and.callThrough();
|
||
|
this.widgetDefinitionsCollection.create.calls.argsFor(0)[1].success();
|
||
|
});
|
||
|
|
||
|
it('should reset orders', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('order')).toEqual([0, 1]);
|
||
|
});
|
||
|
|
||
|
it('should save layers', function () {
|
||
|
expect(this.layerDefinitionsCollection.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should force-reset the styles', function () {
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalledWith(jasmine.anything(), this.layerDefinitionsCollection.at(1), true);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('and does have saved styles', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.widgetDefinitionsCollection, 'create').and.callThrough();
|
||
|
spyOn(this.userActions, '_resetStylePerNode');
|
||
|
|
||
|
this.userActions.createLayerForAnalysisNode('a3', 'a', { at: 2 });
|
||
|
this.widgetDefinitionsCollection.create.calls.argsFor(0)[1].success();
|
||
|
});
|
||
|
|
||
|
it('should not force-reset the styles', function () {
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalled();
|
||
|
expect(this.userActions._resetStylePerNode.calls.argsFor(0)[3]).toBeFalsy();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Node is head of a layer, e.g. given nodeId A3 it should rename prev layer (A => B), and create a new layer (A)
|
||
|
// where the prev layer was to take over its letter identity and its primary source (A2).
|
||
|
// The motivation for this is to maintain the layer's state (styles, popup etc.) which really depends on the
|
||
|
// last analysis output than the layer itself:
|
||
|
// _______ _______ ______
|
||
|
// | A | | A | | B | <-- note that B is really A which just got moved & had it's nodes renamed
|
||
|
// | | | | | |
|
||
|
// | [A2] | => | | | {B1} |
|
||
|
// | [A1] | | [A1] | | [A1] |
|
||
|
// | [A0] | | [A0] | | |
|
||
|
// |______| |______| |______|
|
||
|
describe('when given node is head of a layer', function () {
|
||
|
it('should handle the requests in the correct order', function () {
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'l1',
|
||
|
order: 1,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
table_name: 'foobar',
|
||
|
table_name_alias: 'alias',
|
||
|
cartocss: 'before',
|
||
|
source: 'a2'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Layer pointing to the dragged icon (no definition associated)
|
||
|
this.layerDefinitionsCollection.add({
|
||
|
id: 'l2',
|
||
|
order: 2,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
cartocss: 'before',
|
||
|
source: 'a2'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
spyOn(LayerDefinitionModel.prototype, 'save').and.callThrough();
|
||
|
this.widgetDefinitionsCollection.add({
|
||
|
layer_id: 'l1',
|
||
|
id: 'w2',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'a2'
|
||
|
}
|
||
|
});
|
||
|
this.widgetDefinitionsCollection.add({
|
||
|
layer_id: 'l1',
|
||
|
id: 'w3',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'a1'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
var calls = [];
|
||
|
var prevLayer = this.layerDefinitionsCollection.findWhere({ source: 'a2', letter: 'a' });
|
||
|
|
||
|
prevLayer.sync = function (action, model, opts) {
|
||
|
calls.push('layers');
|
||
|
var deferred = $.Deferred();
|
||
|
deferred.resolve(model.toJSON());
|
||
|
return deferred.promise();
|
||
|
};
|
||
|
|
||
|
_.each(this.analysisDefinitionsCollection.models, function (model) {
|
||
|
model.sync = function (action, model, opts) {
|
||
|
calls.push('analysis');
|
||
|
var deferredZ = $.Deferred();
|
||
|
deferredZ.resolve(model.toJSON());
|
||
|
return deferredZ.promise();
|
||
|
};
|
||
|
}, this);
|
||
|
|
||
|
this.userActions.createLayerForAnalysisNode('a2', 'a', { at: 2 });
|
||
|
expect(calls[0]).toEqual('analysis');
|
||
|
expect(calls[1]).toEqual('layers');
|
||
|
});
|
||
|
|
||
|
describe('without saved styles', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'l1',
|
||
|
order: 1,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
table_name: 'foobar',
|
||
|
table_name_alias: 'alias',
|
||
|
cartocss: 'before',
|
||
|
source: 'a2'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Layer pointing to the dragged icon (no definition associated)
|
||
|
this.layerDefinitionsCollection.add({
|
||
|
id: 'l2',
|
||
|
order: 2,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
cartocss: 'before',
|
||
|
source: 'a2'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
spyOn(LayerDefinitionModel.prototype, 'save').and.callThrough();
|
||
|
this.widgetDefinitionsCollection.add({
|
||
|
layer_id: 'l1',
|
||
|
id: 'w2',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'a2'
|
||
|
}
|
||
|
});
|
||
|
this.widgetDefinitionsCollection.add({
|
||
|
layer_id: 'l1',
|
||
|
id: 'w3',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'a1'
|
||
|
}
|
||
|
});
|
||
|
expect(this.widgetDefinitionsCollection.size()).toBe(3);
|
||
|
});
|
||
|
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.userActions, '_resetStylePerNode');
|
||
|
this.userActions.createLayerForAnalysisNode('a2', 'a', { at: 3 });
|
||
|
});
|
||
|
|
||
|
it('should have created a new layer that takes over the position of layer with head node', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('letter')).toEqual(['a', 'b', 'c']);
|
||
|
});
|
||
|
|
||
|
it('should remove all widgets pointing to the affected node', function () {
|
||
|
expect(this.widgetDefinitionsCollection.size()).toBe(1);
|
||
|
});
|
||
|
|
||
|
it('should change the source layer to appear as the new layer C', function () {
|
||
|
expect(this.layerDefModel.get('letter')).toEqual('c');
|
||
|
expect(this.layerDefModel.get('source')).toEqual('c1');
|
||
|
});
|
||
|
|
||
|
it('should create a new layer which appears to be the source layer A (to preserve styles, popups etc.)', function () {
|
||
|
var newLayerDefModel = this.layerDefinitionsCollection.at(0);
|
||
|
expect(newLayerDefModel.get('source')).toEqual('a1');
|
||
|
expect(newLayerDefModel.get('letter')).toEqual('a');
|
||
|
expect(newLayerDefModel.get('table_name')).toEqual('foobar');
|
||
|
expect(newLayerDefModel.get('table_name_alias')).toEqual('alias');
|
||
|
});
|
||
|
|
||
|
it('should remove old head node', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.get('a2')).toBeUndefined('should no longer exist since replaced by b1');
|
||
|
});
|
||
|
|
||
|
it('should change source of any other layer pointing to that old node and not having any analysis definition', function () {
|
||
|
var layerDefModel = this.layerDefinitionsCollection.findWhere({ id: 'l2' });
|
||
|
expect(layerDefModel.get('source')).toBe('c1');
|
||
|
});
|
||
|
|
||
|
it('should have two analyses, one for each layer', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1', 'c1']);
|
||
|
});
|
||
|
|
||
|
it('should reset styles from the previous layer', function () {
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalled();
|
||
|
expect(this.userActions._resetStylePerNode.calls.argsFor(0)[3]).toBeFalsy();
|
||
|
});
|
||
|
|
||
|
describe('when created a layer successfully', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.widgetDefinitionsCollection, 'create').and.callThrough();
|
||
|
spyOn(this.layerDefinitionsCollection, 'save').and.callThrough();
|
||
|
// New layer is created and saved
|
||
|
LayerDefinitionModel.prototype.save.calls.argsFor(2)[1].success();
|
||
|
});
|
||
|
|
||
|
it('should recreate all affected widgets', function () {
|
||
|
expect(this.widgetDefinitionsCollection.create).toHaveBeenCalled();
|
||
|
expect(this.widgetDefinitionsCollection.create.calls.count()).toBe(2);
|
||
|
var calls = this.widgetDefinitionsCollection.create.calls.all();
|
||
|
var firstWidgetAttrs = calls[0].args[0];
|
||
|
var secondWidgetAttrs = calls[1].args[0];
|
||
|
|
||
|
expect(firstWidgetAttrs).toEqual(
|
||
|
jasmine.objectContaining({
|
||
|
layer_id: 'l1',
|
||
|
type: 'formula',
|
||
|
options: jasmine.objectContaining({
|
||
|
sync_on_bbox_change: true
|
||
|
}),
|
||
|
source: jasmine.objectContaining({
|
||
|
id: 'c1'
|
||
|
}),
|
||
|
avoidNotification: true,
|
||
|
order: 0
|
||
|
})
|
||
|
);
|
||
|
|
||
|
expect(secondWidgetAttrs).toEqual(
|
||
|
jasmine.objectContaining({
|
||
|
source: jasmine.objectContaining({
|
||
|
id: 'a1'
|
||
|
}),
|
||
|
type: 'formula',
|
||
|
options: jasmine.objectContaining({
|
||
|
sync_on_bbox_change: true
|
||
|
}),
|
||
|
avoidNotification: true,
|
||
|
layer_id: undefined,
|
||
|
order: 0
|
||
|
})
|
||
|
);
|
||
|
});
|
||
|
|
||
|
describe('when widgets are created', function () {
|
||
|
beforeEach(function () {
|
||
|
_.each(this.widgetDefinitionsCollection.create.calls.all(), function (call) {
|
||
|
call.args[1].success();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should reset order', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('order')).toEqual([0, 1, 2]);
|
||
|
});
|
||
|
|
||
|
it('should save layers', function () {
|
||
|
expect(this.layerDefinitionsCollection.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should force-reset the styles', function () {
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalledWith(jasmine.anything(), this.layerDefinitionsCollection.at(0), true);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with saved styles', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'layer_with_styles',
|
||
|
order: 1,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
table_name: 'foobar',
|
||
|
table_name_alias: 'alias',
|
||
|
cartocss: 'before',
|
||
|
source: 'a2'
|
||
|
}
|
||
|
});
|
||
|
spyOn(LayerDefinitionModel.prototype, 'save').and.callThrough();
|
||
|
});
|
||
|
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.userActions, '_resetStylePerNode');
|
||
|
this.userActions.createLayerForAnalysisNode('a2', 'a', { at: 2 });
|
||
|
});
|
||
|
|
||
|
describe('when created a layer successfully', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(this.widgetDefinitionsCollection, 'create').and.callThrough();
|
||
|
spyOn(this.layerDefinitionsCollection, 'save').and.callThrough();
|
||
|
LayerDefinitionModel.prototype.save.calls.argsFor(1)[1].success();
|
||
|
});
|
||
|
|
||
|
it('should not force-reset the styles', function () {
|
||
|
expect(this.userActions._resetStylePerNode).toHaveBeenCalled();
|
||
|
expect(this.userActions._resetStylePerNode.calls.argsFor(0)[3]).toBeFalsy();
|
||
|
});
|
||
|
|
||
|
it('should copy styles from analysis node', function () {
|
||
|
var newLayerDefModel = this.layerDefinitionsCollection.at(0);
|
||
|
expect(newLayerDefModel.styleModel.get('type')).toEqual('wadus');
|
||
|
expect(newLayerDefModel.get('cartocss')).toEqual('wadus');
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.moveLayer', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layer1 = this.layerDefinitionsCollection.add({ id: 'layer1', order: 0, kind: 'tiled' });
|
||
|
this.layer2 = this.layerDefinitionsCollection.add({ id: 'layer2', order: 1, kind: 'carto' });
|
||
|
this.layer3 = this.layerDefinitionsCollection.add({ id: 'layer3', order: 2, kind: 'carto' });
|
||
|
this.layer4 = this.layerDefinitionsCollection.add({ id: 'layer4', order: 3, kind: 'carto' });
|
||
|
this.layer5 = this.layerDefinitionsCollection.add({ id: 'layer5', order: 4, kind: 'carto' });
|
||
|
|
||
|
this.layerDefinitionsCollection.reset([
|
||
|
this.layer1,
|
||
|
this.layer2,
|
||
|
this.layer3,
|
||
|
this.layer4,
|
||
|
this.layer5
|
||
|
]);
|
||
|
this.layerDefinitionsCollection.sort();
|
||
|
spyOn(this.layerDefinitionsCollection, 'save').and.callThrough();
|
||
|
|
||
|
this.promise = jasmine.createSpyObj('$.Deferred', ['done', 'fail']);
|
||
|
spyOn($.when, 'apply').and.returnValue(this.promise);
|
||
|
});
|
||
|
|
||
|
it('should reset orders when moving a layer up', function () {
|
||
|
this.userActions.moveLayer({ from: 1, to: 3 });
|
||
|
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual([
|
||
|
'layer1',
|
||
|
'layer3',
|
||
|
'layer4',
|
||
|
'layer2',
|
||
|
'layer5'
|
||
|
]);
|
||
|
|
||
|
this.promise.done.calls.argsFor(0)[0]();
|
||
|
expect(this.layerDefinitionsCollection.pluck('order')).toEqual([0, 1, 2, 3, 4]);
|
||
|
});
|
||
|
|
||
|
it('should reset orders when moving a layer down', function () {
|
||
|
this.userActions.moveLayer({ from: 3, to: 1 });
|
||
|
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual([
|
||
|
'layer1',
|
||
|
'layer4',
|
||
|
'layer2',
|
||
|
'layer3',
|
||
|
'layer5'
|
||
|
]);
|
||
|
|
||
|
this.promise.done.calls.argsFor(0)[0]();
|
||
|
expect(this.layerDefinitionsCollection.pluck('order')).toEqual([0, 1, 2, 3, 4]);
|
||
|
});
|
||
|
|
||
|
it('should save the collection when analyses are saved', function () {
|
||
|
this.userActions.moveLayer({ from: 3, to: 1 });
|
||
|
|
||
|
expect(this.layerDefinitionsCollection.save).not.toHaveBeenCalled();
|
||
|
this.promise.done.calls.argsFor(0)[0]();
|
||
|
expect(this.layerDefinitionsCollection.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should trigger a "layerMoved" event when collection is saved', function () {
|
||
|
var onAddCallback = jasmine.createSpy('onAddCallback');
|
||
|
var onRemoveCallback = jasmine.createSpy('onRemoveCallback');
|
||
|
var onLayerMovedCallback = jasmine.createSpy('onLayerMovedCallback');
|
||
|
|
||
|
this.layerDefinitionsCollection.on('add', onAddCallback);
|
||
|
this.layerDefinitionsCollection.on('remove', onRemoveCallback);
|
||
|
this.layerDefinitionsCollection.on('layerMoved', onLayerMovedCallback);
|
||
|
|
||
|
this.userActions.moveLayer({ from: 3, to: 1 });
|
||
|
|
||
|
this.promise.done.calls.argsFor(0)[0]();
|
||
|
this.layerDefinitionsCollection.save.calls.argsFor(0)[0].success();
|
||
|
expect(onAddCallback).not.toHaveBeenCalled();
|
||
|
expect(onRemoveCallback).not.toHaveBeenCalled();
|
||
|
expect(onLayerMovedCallback).toHaveBeenCalled();
|
||
|
expect(onLayerMovedCallback.calls.argsFor(0)[0].id).toEqual('layer4');
|
||
|
});
|
||
|
|
||
|
it('should create analysis for layers that have a source', function () {
|
||
|
this.analysisDefinitionNodesCollection.createSourceNode({ id: 'a0', tableName: 'foo' });
|
||
|
this.layer2.set('source', 'a0');
|
||
|
this.analysisDefinitionNodesCollection.createSourceNode({ id: 'b0', tableName: 'bar' });
|
||
|
this.layer3.set('source', 'b0');
|
||
|
|
||
|
this.userActions.moveLayer({ from: 3, to: 1 });
|
||
|
|
||
|
this.promise.done.calls.argsFor(0)[0]();
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a0', 'b0']);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.deleteLayer', function () {
|
||
|
beforeEach(function () {
|
||
|
/**
|
||
|
* Layer deletion have a lot of complexity to it, so the before-each has a very complicated setup on purpose,
|
||
|
* to be able to assert the side-effects of various scenarios and corner cases.
|
||
|
*/
|
||
|
this.analysisDefinitionsCollection.add([
|
||
|
{
|
||
|
id: 'for-layerA',
|
||
|
analysis_definition: {
|
||
|
id: 'a1',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
radius: 100,
|
||
|
source: {
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: {
|
||
|
query: 'SELECT * FROM something'
|
||
|
},
|
||
|
options: {
|
||
|
table_name: 'something'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}, {
|
||
|
id: 'for-layerB',
|
||
|
analysis_definition: {
|
||
|
id: 'b2',
|
||
|
type: 'intersection',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'b1',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
radius: 100,
|
||
|
source: {
|
||
|
id: 'b0',
|
||
|
type: 'source',
|
||
|
params: {
|
||
|
query: 'SELECT * FROM something'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
target: {
|
||
|
id: 'a0' // already added before
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}, {
|
||
|
id: 'for-layerC',
|
||
|
analysis_definition: {
|
||
|
id: 'c2', // already added before
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
radius: 100,
|
||
|
source: {
|
||
|
id: 'c1',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
radius: 100,
|
||
|
source: {
|
||
|
id: 'b2' // alredy added before
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}, {
|
||
|
id: 'for-layerD',
|
||
|
analysis_definition: {
|
||
|
id: 'd2',
|
||
|
type: 'intersection',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'd1',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
radius: 100,
|
||
|
source: {
|
||
|
id: 'c1' // already added before
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
target: {
|
||
|
id: 'c2', // already added before
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
radius: 100,
|
||
|
source: {
|
||
|
id: 'c1' // already added before
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
options: {
|
||
|
primary_source_name: 'source'
|
||
|
}
|
||
|
}
|
||
|
}, {
|
||
|
id: 'for-layerE',
|
||
|
analysis_definition: {
|
||
|
id: 'e1',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
radius: 100,
|
||
|
source: {
|
||
|
id: 'c1' // already added --^
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
]);
|
||
|
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add([
|
||
|
{
|
||
|
id: 'basemap',
|
||
|
order: 0,
|
||
|
kind: 'tiled'
|
||
|
}, {
|
||
|
id: 'A',
|
||
|
order: 1,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
table_name: 'something',
|
||
|
cartocss: 'before',
|
||
|
source: 'a1'
|
||
|
}
|
||
|
}, {
|
||
|
id: 'B',
|
||
|
order: 2,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'b',
|
||
|
table_name: 'something',
|
||
|
cartocss: 'before',
|
||
|
source: 'b2'
|
||
|
}
|
||
|
}, {
|
||
|
id: 'C',
|
||
|
order: 3,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'c',
|
||
|
table_name: 'something',
|
||
|
cartocss: 'before',
|
||
|
source: 'c2'
|
||
|
}
|
||
|
}, {
|
||
|
id: 'D',
|
||
|
order: 4,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'd',
|
||
|
table_name: 'something',
|
||
|
cartocss: 'before',
|
||
|
source: 'd2'
|
||
|
}
|
||
|
}, {
|
||
|
id: 'E',
|
||
|
order: 5,
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'e',
|
||
|
table_name: 'something',
|
||
|
cartocss: 'before',
|
||
|
source: 'e1'
|
||
|
}
|
||
|
}
|
||
|
]);
|
||
|
|
||
|
this.widgetDefinitionsCollection.add([
|
||
|
{
|
||
|
id: 'w-b1',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'b1'
|
||
|
}
|
||
|
}, {
|
||
|
id: 'w-b2',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'b2'
|
||
|
}
|
||
|
}, {
|
||
|
id: 'w-c1',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'c1'
|
||
|
}
|
||
|
}, {
|
||
|
id: 'w-d2',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'd2'
|
||
|
}
|
||
|
}, {
|
||
|
id: 'w-e1',
|
||
|
type: 'formula',
|
||
|
source: {
|
||
|
id: 'e1'
|
||
|
}
|
||
|
}
|
||
|
]);
|
||
|
|
||
|
this.promise = jasmine.createSpyObj('$.Deferred', ['done', 'fail']);
|
||
|
spyOn($.when, 'apply').and.returnValue(this.promise);
|
||
|
});
|
||
|
|
||
|
describe('when given a basemap', function () {
|
||
|
beforeEach(function () {
|
||
|
this.userActions.deleteLayer('basemap');
|
||
|
});
|
||
|
|
||
|
it('should throw an error since user should not be able to delete it explicitly', function () {
|
||
|
expect(this.layerDefinitionsCollection.first().id).toEqual('basemap');
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['basemap', 'A', 'B', 'C', 'D', 'E']);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given a layer w/o any dependent nodes (E)', function () {
|
||
|
beforeEach(function () {
|
||
|
this.successSpy = jasmine.createSpy('success');
|
||
|
this.errorSpy = jasmine.createSpy('error');
|
||
|
expect(this.layerDefinitionsCollection.get('E').canBeDeletedByUser()).toBe(true, 'should be able to delete layer');
|
||
|
|
||
|
this.res = this.userActions.deleteLayer('E', {
|
||
|
success: this.successSpy,
|
||
|
error: this.errorSpy
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should delete the layer', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['basemap', 'A', 'B', 'C', 'D'], 'should exclude E');
|
||
|
});
|
||
|
|
||
|
it('should delete affected widgets', function () {
|
||
|
expect(this.widgetDefinitionsCollection.pluck('source')).toEqual(['b1', 'b2', 'c1', 'd2'], 'should exlude e1');
|
||
|
});
|
||
|
|
||
|
it('should have persisted the remaining layers', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1', 'b2', 'c2', 'd2'], 'should delete e1');
|
||
|
});
|
||
|
|
||
|
it('should delete orphaned nodes', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id').sort()).toEqual(['a0', 'a1', 'b0', 'b1', 'b2', 'c1', 'c2', 'd1', 'd2'], 'should exclude e1');
|
||
|
});
|
||
|
|
||
|
it('should return a promise', function () {
|
||
|
expect(this.res).toBe(this.promise);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given a layer w/o any dependent nodes (D)', function () {
|
||
|
beforeEach(function () {
|
||
|
expect(this.layerDefinitionsCollection.get('D').canBeDeletedByUser()).toBe(true, 'should be able to delete layer');
|
||
|
this.userActions.deleteLayer('D');
|
||
|
});
|
||
|
|
||
|
it('should delete the layer', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['basemap', 'A', 'B', 'C', 'E'], 'should exclude D');
|
||
|
});
|
||
|
|
||
|
it('should delete affected widgets', function () {
|
||
|
expect(this.widgetDefinitionsCollection.pluck('source')).toEqual(['b1', 'b2', 'c1', 'e1'], 'should exclude d2');
|
||
|
});
|
||
|
|
||
|
it('should persist analyses', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1', 'b2', 'c2', 'e1'], 'should exclude d2');
|
||
|
});
|
||
|
|
||
|
it('should delete orphaned nodes', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id').sort()).toEqual(['a0', 'a1', 'b0', 'b1', 'b2', 'c1', 'c2', 'e1'], 'should exclude [d2, d2]');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given a layer with a primary dependent layer further down the linked nodes list (C)', function () {
|
||
|
beforeEach(function () {
|
||
|
expect(this.layerDefinitionsCollection.get('C').canBeDeletedByUser()).toBe(true, 'should be able to delete layer');
|
||
|
this.userActions.deleteLayer('C');
|
||
|
});
|
||
|
|
||
|
it('should delete affected layers', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['basemap', 'A', 'B', 'E'], 'should exclude [C,D]');
|
||
|
});
|
||
|
|
||
|
it('should update the parent layer found with new source', function () {
|
||
|
expect(this.layerDefinitionsCollection.get('E').get('source')).toEqual('e2');
|
||
|
});
|
||
|
|
||
|
it('should updated affected widgets', function () {
|
||
|
expect(this.widgetDefinitionsCollection
|
||
|
.map(function (m) {
|
||
|
return [m.id, m.get('source')];
|
||
|
}))
|
||
|
.toEqual([['w-b1', 'b1'], ['w-b2', 'b2'], ['w-c1', 'e1'], ['w-e1', 'e2']], 'should exclude [c2, d2], and changed w-e1 => e2');
|
||
|
});
|
||
|
|
||
|
it('should persist analyses and deleted the analysis of the ones that are no longer needed', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1', 'b2', 'e2']);
|
||
|
});
|
||
|
|
||
|
it('should delete orphaned nodes', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id').sort()).toEqual(['a0', 'a1', 'b0', 'b1', 'b2', 'e1', 'e2'], 'should exclude [c2, d2, d2]');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given layer with a primary dependent layer on the head node (B)', function () {
|
||
|
beforeEach(function () {
|
||
|
expect(this.layerDefinitionsCollection.get('B').canBeDeletedByUser()).toBe(true, 'should be able to delete layer');
|
||
|
this.userActions.deleteLayer('B');
|
||
|
});
|
||
|
|
||
|
it('should delete layer', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['basemap', 'A', 'C', 'D', 'E'], 'should exclude B only');
|
||
|
});
|
||
|
|
||
|
it('should update the parent layer found with new source', function () {
|
||
|
expect(this.layerDefinitionsCollection.get('C').get('source')).toEqual('c4');
|
||
|
});
|
||
|
|
||
|
it('should updated affected widgets', function () {
|
||
|
expect(this.widgetDefinitionsCollection
|
||
|
.map(function (m) {
|
||
|
return [m.id, m.get('source')];
|
||
|
}))
|
||
|
.toEqual([['w-b1', 'c1'], ['w-b2', 'c2'], ['w-c1', 'c3'], ['w-d2', 'd2'], ['w-e1', 'e1']], 'should exclude update sources');
|
||
|
});
|
||
|
|
||
|
it('should persist analyses', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1', 'c4', 'd2', 'e1']);
|
||
|
});
|
||
|
|
||
|
it('should delete orphaned nodes', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id').sort()).toEqual(['a0', 'a1', 'c0', 'c1', 'c2', 'c3', 'c4', 'd1', 'd2', 'e1'], 'should exclude [b1, b2]');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given a layer which all other layers depend on (A)', function () {
|
||
|
beforeEach(function () {
|
||
|
spyOn(Backbone.Model.prototype, 'destroy');
|
||
|
spyOn(Backbone.Model.prototype, 'save');
|
||
|
spyOn(Backbone.Collection.prototype, 'create');
|
||
|
|
||
|
expect(this.layerDefinitionsCollection.get('A').canBeDeletedByUser()).toBe(false);
|
||
|
this.userActions.deleteLayer('A');
|
||
|
});
|
||
|
|
||
|
it('should not delete anything', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['basemap', 'A', 'B', 'C', 'D', 'E']);
|
||
|
|
||
|
expect(Backbone.Model.prototype.destroy).not.toHaveBeenCalled();
|
||
|
expect(Backbone.Model.prototype.save).not.toHaveBeenCalled();
|
||
|
expect(Backbone.Collection.prototype.create).not.toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when having a layer which source is owned by another layer', function () {
|
||
|
// _______ ______ ______
|
||
|
// | X | | Y | | Z |
|
||
|
// | | | | | |
|
||
|
// | [x0] | | [x0] | | [x0] | <-- all layers share the x0 node owned by layer X
|
||
|
// |______| |______| |______| test deleting layer X and Y and assert side-effects:
|
||
|
beforeEach(function () {
|
||
|
this.analysisDefinitionNodesCollection.createSourceNode({ id: 'x0', tableName: 'xena' });
|
||
|
this.layerX = this.layerDefinitionsCollection.add({
|
||
|
id: 'X',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'x',
|
||
|
source: 'x0'
|
||
|
}
|
||
|
});
|
||
|
this.layerY = this.layerDefinitionsCollection.add({
|
||
|
id: 'Y',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'y',
|
||
|
source: 'x0' // ref to ^^^
|
||
|
}
|
||
|
});
|
||
|
this.layerZ = this.layerDefinitionsCollection.add({
|
||
|
id: 'Z',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'z',
|
||
|
source: 'x0' // ref to ^^^
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when deleting a layer which node is a reference to other source node of other layer', function () {
|
||
|
beforeEach(function () {
|
||
|
this.userActions.deleteLayer('Y');
|
||
|
});
|
||
|
|
||
|
it('should just delete the layer and leave the nodes alone', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['basemap', 'A', 'B', 'C', 'D', 'E', 'X', 'Z'], 'should exclude Y');
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id').sort()).toEqual(['a0', 'a1', 'b0', 'b1', 'b2', 'c1', 'c2', 'd1', 'd2', 'e1', 'x0'], 'should include x0 in addition to the defaults');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when deleting the other layer which owns the shared source node', function () {
|
||
|
beforeEach(function () {
|
||
|
this.userActions.deleteLayer('X');
|
||
|
});
|
||
|
|
||
|
it('should delete the layer of given id', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['basemap', 'A', 'B', 'C', 'D', 'E', 'Y', 'Z'], 'should exclude X');
|
||
|
});
|
||
|
|
||
|
it('should move the analysis node to one of the other layers', function () {
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id').sort()).toEqual(['a0', 'a1', 'b0', 'b1', 'b2', 'c1', 'c2', 'd1', 'd2', 'e1', 'y0'], 'should moved x0 to y0');
|
||
|
expect(this.layerDefinitionsCollection.pluck('source')).toEqual([undefined, 'a1', 'b2', 'c2', 'd2', 'e1', 'y0', 'y0']);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// https://github.com/CartoDB/cartodb/issues/9168
|
||
|
describe('when a layer has nodes with larger ids than there are nodes', function () {
|
||
|
beforeEach(function () {
|
||
|
this.analysisDefinitionNodesCollection.reset();
|
||
|
this.analysisDefinitionsCollection.reset();
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: '41ceca6d-1b59-45e1-ab9b-0b092fc5cb9d',
|
||
|
analysis_definition: {
|
||
|
id: 'a1',
|
||
|
type: 'kmeans',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'a0',
|
||
|
type: 'source',
|
||
|
params: {
|
||
|
query: 'SELECT * FROM crime_incidents_2016_5'
|
||
|
},
|
||
|
options: {
|
||
|
table_name: 'crime_incidents_2016_5'
|
||
|
}
|
||
|
},
|
||
|
clusters: 3
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: '5fb42f6d-debb-4b0c-bf37-daac79639b57',
|
||
|
analysis_definition: {
|
||
|
id: 'b4',
|
||
|
type: 'intersection',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'b3',
|
||
|
type: 'buffer',
|
||
|
params: {
|
||
|
source: {
|
||
|
id: 'c2',
|
||
|
type: 'centroid',
|
||
|
params: {
|
||
|
source: { id: 'a1' },
|
||
|
category_column: 'cluster_no'
|
||
|
}
|
||
|
},
|
||
|
radius: 750,
|
||
|
isolines: 1,
|
||
|
dissolved: false
|
||
|
},
|
||
|
options: {
|
||
|
kind: 'car',
|
||
|
time: '300',
|
||
|
distance: 'meters'
|
||
|
}
|
||
|
},
|
||
|
target: { id: 'a1' }
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: '312a9e2a-8c59-406b-bd77-588e7e98ebef',
|
||
|
analysis_definition: { id: 'c2' }
|
||
|
});
|
||
|
this.layerDefinitionsCollection.reset();
|
||
|
this.layerA = this.layerDefinitionsCollection.add({
|
||
|
id: 'A',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
source: 'a1'
|
||
|
}
|
||
|
});
|
||
|
this.layerB = this.layerDefinitionsCollection.add({
|
||
|
id: 'B',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'b',
|
||
|
source: 'b4'
|
||
|
}
|
||
|
});
|
||
|
this.layerZ = this.layerDefinitionsCollection.add({
|
||
|
id: 'C',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'c',
|
||
|
source: 'c2'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.userActions.deleteLayer('C');
|
||
|
});
|
||
|
|
||
|
it('should fold node c2 under layer B', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['A', 'B'], 'layer C should be deleted');
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).toEqual(['a0', 'a1', 'b2', 'b3', 'b4'], 'c2 should not be b2');
|
||
|
expect(this.analysisDefinitionNodesCollection.get('b3').get('source')).toEqual('b2', 'b3 should point on the renamed node');
|
||
|
expect(this.analysisDefinitionNodesCollection.get('b2').get('source')).toEqual('a1', 'b2 should still point to the same node');
|
||
|
expect(this.analysisDefinitionNodesCollection.get('b2').get('type')).toEqual('centroid', 'b2 should be the prev c2');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// https://github.com/CartoDB/cartodb/issues/10366
|
||
|
describe('when a layer has to fold things into only one layer', function () {
|
||
|
beforeEach(function () {
|
||
|
this.analysisDefinitionNodesCollection.reset();
|
||
|
this.analysisDefinitionsCollection.reset();
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: '41ceca6d-1b59-45e1-ab9b-0b092fc5cb9d',
|
||
|
analysis_definition: {
|
||
|
id: 'a2',
|
||
|
type: 'centroid',
|
||
|
params: {
|
||
|
category_column: 'cluster_no',
|
||
|
source: {
|
||
|
id: 'b1',
|
||
|
type: 'kmeans',
|
||
|
params: {
|
||
|
clusters: 3,
|
||
|
source: {
|
||
|
id: 'b0',
|
||
|
type: 'source',
|
||
|
options: {
|
||
|
table_name: 'ec_kvy'
|
||
|
},
|
||
|
params: {
|
||
|
query: 'SELECT * FROM ec_kvy'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.analysisDefinitionsCollection.add({
|
||
|
id: '5fb42f6d-debb-4b0c-bf37-daac79639b57',
|
||
|
analysis_definition: {
|
||
|
id: 'b1',
|
||
|
type: 'kmeans',
|
||
|
params: {
|
||
|
clusters: 2,
|
||
|
source: {
|
||
|
id: 'b0',
|
||
|
type: 'source',
|
||
|
options: {
|
||
|
table_name: 'ec_kvy'
|
||
|
},
|
||
|
params: {
|
||
|
query: 'SELECT * FROM ec_kvy'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.layerDefinitionsCollection.reset();
|
||
|
this.layerA = this.layerDefinitionsCollection.add({
|
||
|
id: 'A',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
source: 'a2'
|
||
|
}
|
||
|
});
|
||
|
this.layerB = this.layerDefinitionsCollection.add({
|
||
|
id: 'B',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'b',
|
||
|
source: 'b1'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.userActions.deleteLayer('B');
|
||
|
});
|
||
|
|
||
|
it('should fold node b1 and b0 under layer A', function () {
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['A'], 'layer B should be deleted');
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).toEqual(['a0', 'a1', 'a2'], 'b0 and b1 should not be removed, and b1 should be a1 and b0 should be a0');
|
||
|
expect(this.analysisDefinitionNodesCollection.get('a1').get('source')).toEqual('a0', 'b1 should point on the renamed node');
|
||
|
expect(this.analysisDefinitionNodesCollection.get('a2').get('source')).toEqual('a1');
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.saveLayer', function () {
|
||
|
describe('when given layer is a non-data layer', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'basemap',
|
||
|
kind: 'tiled'
|
||
|
});
|
||
|
this.layerDefModel.set('dirty', true);
|
||
|
spyOn(this.layerDefModel, 'save').and.callThrough();
|
||
|
|
||
|
this.res = this.userActions.saveLayer(this.layerDefModel);
|
||
|
});
|
||
|
|
||
|
it('should only save layer', function () {
|
||
|
expect(this.layerDefModel.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should return a promise', function () {
|
||
|
expect(this.res).toBeDefined();
|
||
|
expect(this.res.done).toEqual(jasmine.any(Function));
|
||
|
expect(this.res.fail).toEqual(jasmine.any(Function));
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given layer is a data layer', function () {
|
||
|
beforeEach(function () {
|
||
|
this.layerDefModel = this.layerDefinitionsCollection.add({
|
||
|
id: 'A',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
cartocss: '',
|
||
|
table_name: 'foobar'
|
||
|
}
|
||
|
});
|
||
|
this.layerDefModel.set('dirty', true);
|
||
|
spyOn(this.layerDefModel, 'save').and.callThrough();
|
||
|
|
||
|
this.res = this.userActions.saveLayer(this.layerDefModel);
|
||
|
});
|
||
|
|
||
|
it('should layer', function () {
|
||
|
expect(this.layerDefModel.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should save analysis', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a0']);
|
||
|
});
|
||
|
|
||
|
it('should return a promise', function () {
|
||
|
expect(this.res).toBeDefined();
|
||
|
expect(this.res.done).toEqual(jasmine.any(Function));
|
||
|
expect(this.res.fail).toEqual(jasmine.any(Function));
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.updateWidgetsOrder', function () {
|
||
|
it('should call the collection method to update widgets order ', function () {
|
||
|
spyOn(this.widgetDefinitionsCollection, 'updateWidgetsOrder');
|
||
|
|
||
|
this.userActions.updateWidgetsOrder();
|
||
|
|
||
|
expect(this.widgetDefinitionsCollection.updateWidgetsOrder).toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('.saveWidget', function () {
|
||
|
beforeEach(function () {
|
||
|
this.A = this.layerDefinitionsCollection.add({
|
||
|
id: 'A',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'a',
|
||
|
table_name: 'alice'
|
||
|
}
|
||
|
});
|
||
|
this.B = this.layerDefinitionsCollection.add({
|
||
|
id: 'B',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
letter: 'b',
|
||
|
table_name: 'bob'
|
||
|
}
|
||
|
});
|
||
|
spyOn(this.A, 'save').and.callThrough();
|
||
|
spyOn(this.B, 'save').and.callThrough();
|
||
|
|
||
|
this.wa0 = this.widgetDefinitionsCollection.add({
|
||
|
id: 'wa0',
|
||
|
type: 'formula',
|
||
|
source: { id: 'a0' }
|
||
|
});
|
||
|
this.wa0.set('dirty', true);
|
||
|
|
||
|
spyOn(this.wa0, 'save').and.callThrough();
|
||
|
});
|
||
|
|
||
|
describe('when given a widget which is not tied to a layer', function () {
|
||
|
beforeEach(function () {
|
||
|
this.userActions.saveWidget(this.wa0);
|
||
|
});
|
||
|
|
||
|
it('should only save widget', function () {
|
||
|
expect(this.wa0.save).toHaveBeenCalled();
|
||
|
expect(this.A.save).not.toHaveBeenCalled();
|
||
|
expect(this.B.save).not.toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when given a widget which is tied to a layer', function () {
|
||
|
beforeEach(function () {
|
||
|
this.wa0.set('layer_id', 'A');
|
||
|
this.userActions.saveWidget(this.wa0);
|
||
|
});
|
||
|
|
||
|
it('should save widget', function () {
|
||
|
expect(this.wa0.save).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should not save the layer, it is not necessary', function () {
|
||
|
expect(this.A.save).not.toHaveBeenCalled();
|
||
|
expect(this.B.save).not.toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it('should save analysis', function () {
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a0']);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('smoke tests', function () {
|
||
|
beforeEach(function () {
|
||
|
this.analysisSourceOptionsModel = new AnalysisSourceOptionsModel(null, {
|
||
|
analysisDefinitionNodesCollection: this.analysisDefinitionNodesCollection,
|
||
|
layerDefinitionsCollection: this.layerDefinitionsCollection,
|
||
|
tablesCollection: new Backbone.Collection()
|
||
|
});
|
||
|
|
||
|
spyOn(CDB.SQL.prototype, 'execute').and.callFake(function (query, vars, params) {
|
||
|
params && params.success({
|
||
|
rows: [],
|
||
|
fields: []
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('Metro Madrid use case', function () {
|
||
|
this.configModel.dataServiceEnabled = function () {
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
// create a new map w/ paradas_metro_madrid dataset, playing with styles
|
||
|
this.basemap = this.layerDefinitionsCollection.add({
|
||
|
id: 'basemap',
|
||
|
kind: 'tiled'
|
||
|
});
|
||
|
this.layerA = this.layerDefinitionsCollection.add({
|
||
|
id: 'layerA',
|
||
|
kind: 'carto',
|
||
|
options: {
|
||
|
table_name: 'paradas_metro_madrid'
|
||
|
}
|
||
|
});
|
||
|
this.labelsOnTop = this.layerDefinitionsCollection.add({
|
||
|
id: 'labels-on-top',
|
||
|
kind: 'tiled'
|
||
|
});
|
||
|
expect(this.layerA.get('letter')).toEqual('a', 'should have a letter representation');
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual([], 'analysis is not persisted initially, for backward compability with old editor');
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).toEqual(['a0'], 'should have a node created implicitly for the table of layerA');
|
||
|
|
||
|
// First, Play with the styles for this layer
|
||
|
this.layerA.styleModel.setDefaultPropertiesByType('simple', 'point');
|
||
|
this.userActions.saveLayer(this.layerA);
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a0'], 'should persist the analysis when layer is saved');
|
||
|
|
||
|
// Add "area of influence" analysis by distance 1km to your layer
|
||
|
var aFormModel = new AreaOfInfluenceFormModel({
|
||
|
id: 'a1',
|
||
|
type: 'buffer',
|
||
|
radius: '1000',
|
||
|
source: 'a0',
|
||
|
distance: 'kilometers'
|
||
|
}, {
|
||
|
analyses: analyses,
|
||
|
configModel: this.configModel,
|
||
|
layerDefinitionModel: this.layerA,
|
||
|
analysisSourceOptionsModel: {}
|
||
|
});
|
||
|
this.userActions.saveAnalysis(aFormModel);
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).toEqual(['a0', 'a1'], 'should create a new node');
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a1'], 'should update analysis of layer to point to new head node');
|
||
|
|
||
|
// Go to the layer list and drag the AOI node outside the current layer to create a new one.
|
||
|
this.userActions.createLayerForAnalysisNode('a1', 'a', { at: 1 });
|
||
|
expect(this.layerDefinitionsCollection.pluck('letter')).toEqual([undefined, 'b', 'a', undefined], 'should have a new letter representation b for new layer');
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).toEqual(['a0', 'b1'], 'should replaced node a1 with b1');
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a0', 'b1'], 'should updated prev layer to have prev node (a0), and created a new analysis for new layer (b1)');
|
||
|
|
||
|
// Play with the styles for the AOI layer.
|
||
|
this.layerA.styleModel.setDefaultPropertiesByType('squares', 'point');
|
||
|
this.userActions.saveLayer(this.layerA);
|
||
|
|
||
|
// Add a Category Widget to 'Line' column on A0 (Metro stations layer Data Source)
|
||
|
var categoryWidgetOption = new CategoryWidgetOptionModel({
|
||
|
type: 'category',
|
||
|
layer_index: '0',
|
||
|
title: 'default-title',
|
||
|
tuples: [{
|
||
|
columnModel: new Backbone.Model({ name: 'line' }),
|
||
|
layerDefinitionModel: this.layerA,
|
||
|
analysisDefinitionNodeModel: this.analysisDefinitionNodesCollection.get('a0')
|
||
|
}]
|
||
|
});
|
||
|
interceptAjaxCall = function (params) {
|
||
|
if (/widgets/.test(params.url)) {
|
||
|
params.success && params.success({
|
||
|
id: 'a0-line-category',
|
||
|
type: 'category',
|
||
|
title: 'default-title',
|
||
|
order: 0,
|
||
|
layer_id: 'layerA',
|
||
|
options: {
|
||
|
column: 'line',
|
||
|
aggregation_column: 'line',
|
||
|
aggregation: 'count',
|
||
|
sync_on_bbox_change: true
|
||
|
},
|
||
|
source: { id: 'a0' }
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
this.userActions.saveWidgetOption(categoryWidgetOption);
|
||
|
expect(this.widgetDefinitionsCollection.pluck('title')).toEqual(['default-title']);
|
||
|
expect(this.widgetDefinitionsCollection.pluck('id')).toEqual(['a0-line-category']);
|
||
|
|
||
|
// Change the widget name to "Stations per Line"
|
||
|
var lineCategoryWidget = this.widgetDefinitionsCollection.get('a0-line-category');
|
||
|
lineCategoryWidget.set('title', 'Stations per line');
|
||
|
interceptAjaxCall = function (params) {
|
||
|
if (/widgets/.test(params.url)) {
|
||
|
params.success && params.success({
|
||
|
id: 'a0-line-category',
|
||
|
title: 'Stations per line'
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
this.userActions.saveWidget(lineCategoryWidget);
|
||
|
expect(this.widgetDefinitionsCollection.pluck('title')).toEqual(['Stations per line']);
|
||
|
|
||
|
// Skipped these because can't do the assertions of cartodb.js here
|
||
|
// Filter by L1 on the Widget. This will only show metro stations and AOIs for that line (Isn't this cool??)
|
||
|
// Add a Category Widget to 'Name' column on A0 (Metro stations layer Data Source)
|
||
|
|
||
|
// A new Data Layer with lines should appear in your layer list now.
|
||
|
interceptAjaxCall = function (params) {
|
||
|
if (/layers/.test(params.url)) {
|
||
|
params.success && params.success({
|
||
|
id: 'metro_lines',
|
||
|
order: 0,
|
||
|
infowindow: {},
|
||
|
tooltip: {},
|
||
|
kind: 'carto'
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
var tableModel = new TableModel({ name: 'metro_lines' }, { configModel: this.configModel });
|
||
|
this.userActions.createLayerFromTable(tableModel);
|
||
|
expect(this.layerDefinitionsCollection.pluck('id')).toEqual(['basemap', 'layerA', undefined, 'metro_lines', 'labels-on-top']);
|
||
|
expect(this.layerDefinitionsCollection.pluck('source')).toEqual([undefined, 'b1', 'a0', 'c0', undefined]);
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a0', 'b1', 'c0']);
|
||
|
|
||
|
aFormModel = new FilterByNodeColumnFormModel({
|
||
|
id: 'c1',
|
||
|
type: 'filter-by-node-column',
|
||
|
source: 'c0',
|
||
|
column: 'name',
|
||
|
filter_source: 'b1',
|
||
|
filter_column: 'line'
|
||
|
}, {
|
||
|
analyses: analyses,
|
||
|
configModel: this.configModel,
|
||
|
layerDefinitionModel: this.layerDefinitionsCollection.findWhere({ letter: 'c' }),
|
||
|
analysisSourceOptionsModel: this.analysisSourceOptionsModel
|
||
|
});
|
||
|
this.userActions.saveAnalysis(aFormModel);
|
||
|
expect(this.analysisDefinitionNodesCollection.pluck('id')).toEqual(['a0', 'b1', 'c0', 'c1'], 'should add new node c1');
|
||
|
expect(this.analysisDefinitionsCollection.pluck('node_id')).toEqual(['a0', 'b1', 'c1'], 'should updated existing analysis (c0 => c1)');
|
||
|
|
||
|
// Stopping here, since remaining stuff can't be verified anyway
|
||
|
});
|
||
|
});
|
||
|
});
|