mirror of https://github.com/artf/grapesjs.git
Browse Source
* Avoid throwing errors if no condition is passed to a DataCondition * Refactor Condition class * Remove expecting errors for conditions tests * Extend and refactor BaseOperator * Rename and refactor GenericOperator * Rename and refactor logicalOperator to BooleanOperator * Refactor StringOperator * Refactor NumberOperator * Refactor and fix DataResolverListener * Update tests for condition Operators * Rename Condition class to ConditionEvaluator * Add missing types file * Update and refactor DataCondition * Update utils * Refactor StyleableModel class * Update ComponentDataCondition * Refactor ComponentResolverWatcher * Fix conditional styles * Rename LogicalGroupStatement to LogicalGroupEvaluator * Fix tests for DataCondition * Add setter methods for component data variable * Add setters and getter to ComponentDataCondition * Add getters to ComponentDataVariable * Rename test file * Make dynamic components undroppable * Fix collection types * Update collections ( add setters and getter, first item editable, sync styles ) * Update data collection tests * Format tests * Fix some ComponentData collection bugs * Refactor setStartIndex and setEndIndex * Fix getComponentDef test * Fix bug with end index = 0 * fix test for setDataSource * fix test for HTML updates * Format * Add tests for the new option in getDataValue for conditions ( skipDynamicValueResolution ) * rename Operation to DataConditionOperation * Run __onStyleChange after style changes as before * Format * Up BooleanOperator --------- Co-authored-by: Artur Arseniev <artur.catch@hotmail.it>up-docs-banner
committed by
GitHub
32 changed files with 1326 additions and 498 deletions
@ -0,0 +1,14 @@ |
|||||
|
import EditorModel from '../../../../editor/model/Editor'; |
||||
|
import { DataConditionOperation } from './types'; |
||||
|
|
||||
|
export abstract class Operator<OperationType extends DataConditionOperation> { |
||||
|
protected em: EditorModel; |
||||
|
protected operation: OperationType; |
||||
|
|
||||
|
constructor(operation: any, opts: { em: EditorModel }) { |
||||
|
this.operation = operation; |
||||
|
this.em = opts.em; |
||||
|
} |
||||
|
|
||||
|
abstract evaluate(left: any, right: any): boolean; |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
import { Operator } from './BaseOperator'; |
||||
|
|
||||
|
export enum BooleanOperation { |
||||
|
and = 'and', |
||||
|
or = 'or', |
||||
|
xor = 'xor', |
||||
|
} |
||||
|
|
||||
|
export class BooleanOperator extends Operator<BooleanOperation> { |
||||
|
evaluate(statements: boolean[]): boolean { |
||||
|
if (!statements?.length) return false; |
||||
|
|
||||
|
switch (this.operation) { |
||||
|
case BooleanOperation.and: |
||||
|
return statements.every(Boolean); |
||||
|
case BooleanOperation.or: |
||||
|
return statements.some(Boolean); |
||||
|
case BooleanOperation.xor: |
||||
|
return statements.filter(Boolean).length === 1; |
||||
|
default: |
||||
|
this.em.logError(`Unsupported boolean operation: ${this.operation}`); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,28 +0,0 @@ |
|||||
import { Operator } from '.'; |
|
||||
|
|
||||
export enum LogicalOperation { |
|
||||
and = 'and', |
|
||||
or = 'or', |
|
||||
xor = 'xor', |
|
||||
} |
|
||||
|
|
||||
export class LogicalOperator extends Operator { |
|
||||
constructor(private operator: LogicalOperation) { |
|
||||
super(); |
|
||||
} |
|
||||
|
|
||||
evaluate(statements: boolean[]): boolean { |
|
||||
if (!statements.length) throw new Error('Expected one or more statements, got none'); |
|
||||
|
|
||||
switch (this.operator) { |
|
||||
case LogicalOperation.and: |
|
||||
return statements.every(Boolean); |
|
||||
case LogicalOperation.or: |
|
||||
return statements.some(Boolean); |
|
||||
case LogicalOperation.xor: |
|
||||
return statements.filter(Boolean).length === 1; |
|
||||
default: |
|
||||
throw new Error(`Unsupported logical operator: ${this.operator}`); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,3 +0,0 @@ |
|||||
export abstract class Operator { |
|
||||
abstract evaluate(left: any, right: any): boolean; |
|
||||
} |
|
||||
@ -0,0 +1,6 @@ |
|||||
|
import { AnyTypeOperation } from './AnyTypeOperator'; |
||||
|
import { BooleanOperation } from './BooleanOperator'; |
||||
|
import { NumberOperation } from './NumberOperator'; |
||||
|
import { StringOperation } from './StringOperator'; |
||||
|
|
||||
|
export type DataConditionOperation = AnyTypeOperation | StringOperation | NumberOperation | BooleanOperation; |
||||
@ -0,0 +1,94 @@ |
|||||
|
import { DataSourceManager } from '../../../../src'; |
||||
|
import ComponentDataVariable from '../../../../src/data_sources/model/ComponentDataVariable'; |
||||
|
import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; |
||||
|
import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; |
||||
|
import EditorModel from '../../../../src/editor/model/Editor'; |
||||
|
import { setupTestEditor } from '../../../common'; |
||||
|
|
||||
|
describe('ComponentDataVariable - setPath and setDefaultValue', () => { |
||||
|
let em: EditorModel; |
||||
|
let dsm: DataSourceManager; |
||||
|
let cmpRoot: ComponentWrapper; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
({ em, dsm, cmpRoot } = setupTestEditor()); |
||||
|
const dataSource = { |
||||
|
id: 'ds_id', |
||||
|
records: [ |
||||
|
{ id: 'id1', name: 'Name1' }, |
||||
|
{ id: 'id2', name: 'Name2' }, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
dsm.add(dataSource); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
em.destroy(); |
||||
|
}); |
||||
|
|
||||
|
test('component updates when path is changed using setPath', () => { |
||||
|
const cmp = cmpRoot.append({ |
||||
|
type: DataVariableType, |
||||
|
defaultValue: 'default', |
||||
|
path: 'ds_id.id1.name', |
||||
|
})[0] as ComponentDataVariable; |
||||
|
|
||||
|
expect(cmp.getEl()?.innerHTML).toContain('Name1'); |
||||
|
expect(cmp.getPath()).toBe('ds_id.id1.name'); |
||||
|
|
||||
|
cmp.setPath('ds_id.id2.name'); |
||||
|
expect(cmp.getEl()?.innerHTML).toContain('Name2'); |
||||
|
expect(cmp.getPath()).toBe('ds_id.id2.name'); |
||||
|
}); |
||||
|
|
||||
|
test('component updates when default value is changed using setDefaultValue', () => { |
||||
|
const cmp = cmpRoot.append({ |
||||
|
type: DataVariableType, |
||||
|
defaultValue: 'default', |
||||
|
path: 'unknown.id1.name', |
||||
|
})[0] as ComponentDataVariable; |
||||
|
|
||||
|
expect(cmp.getEl()?.innerHTML).toContain('default'); |
||||
|
expect(cmp.getDefaultValue()).toBe('default'); |
||||
|
|
||||
|
cmp.setDefaultValue('new default'); |
||||
|
expect(cmp.getEl()?.innerHTML).toContain('new default'); |
||||
|
expect(cmp.getDefaultValue()).toBe('new default'); |
||||
|
}); |
||||
|
|
||||
|
test('component updates correctly after path and default value are changed', () => { |
||||
|
const cmp = cmpRoot.append({ |
||||
|
type: DataVariableType, |
||||
|
defaultValue: 'default', |
||||
|
path: 'ds_id.id1.name', |
||||
|
})[0] as ComponentDataVariable; |
||||
|
|
||||
|
expect(cmp.getEl()?.innerHTML).toContain('Name1'); |
||||
|
|
||||
|
cmp.setPath('ds_id.id2.name'); |
||||
|
expect(cmp.getEl()?.innerHTML).toContain('Name2'); |
||||
|
|
||||
|
cmp.setDefaultValue('new default'); |
||||
|
dsm.all.reset(); |
||||
|
expect(cmp.getEl()?.innerHTML).toContain('new default'); |
||||
|
expect(cmp.getDefaultValue()).toBe('new default'); |
||||
|
}); |
||||
|
|
||||
|
test('component updates correctly after path is changed and data is updated', () => { |
||||
|
const cmp = cmpRoot.append({ |
||||
|
type: DataVariableType, |
||||
|
defaultValue: 'default', |
||||
|
path: 'ds_id.id1.name', |
||||
|
})[0] as ComponentDataVariable; |
||||
|
|
||||
|
expect(cmp.getEl()?.innerHTML).toContain('Name1'); |
||||
|
|
||||
|
cmp.setPath('ds_id.id2.name'); |
||||
|
expect(cmp.getEl()?.innerHTML).toContain('Name2'); |
||||
|
|
||||
|
const ds = dsm.get('ds_id'); |
||||
|
ds.getRecord('id2')?.set({ name: 'Name2-UP' }); |
||||
|
expect(cmp.getEl()?.innerHTML).toContain('Name2-UP'); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,156 @@ |
|||||
|
import { Component, DataSourceManager, Editor } from '../../../../../src'; |
||||
|
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; |
||||
|
import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition'; |
||||
|
import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; |
||||
|
import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; |
||||
|
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; |
||||
|
import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView'; |
||||
|
import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; |
||||
|
import EditorModel from '../../../../../src/editor/model/Editor'; |
||||
|
import { setupTestEditor } from '../../../../common'; |
||||
|
|
||||
|
describe('ComponentDataCondition Setters', () => { |
||||
|
let editor: Editor; |
||||
|
let em: EditorModel; |
||||
|
let dsm: DataSourceManager; |
||||
|
let cmpRoot: ComponentWrapper; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
({ editor, em, dsm, cmpRoot } = setupTestEditor()); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
em.destroy(); |
||||
|
}); |
||||
|
|
||||
|
it('should update the condition using setCondition', () => { |
||||
|
const component = cmpRoot.append({ |
||||
|
type: DataConditionType, |
||||
|
condition: { |
||||
|
left: 0, |
||||
|
operator: NumberOperation.greaterThan, |
||||
|
right: -1, |
||||
|
}, |
||||
|
ifTrue: '<h1>some text</h1>', |
||||
|
ifFalse: '<h1>false text</h1>', |
||||
|
})[0] as ComponentDataCondition; |
||||
|
|
||||
|
const newCondition = { |
||||
|
left: 1, |
||||
|
operator: NumberOperation.lessThan, |
||||
|
right: 0, |
||||
|
}; |
||||
|
|
||||
|
component.setCondition(newCondition); |
||||
|
expect(component.getCondition()).toEqual(newCondition); |
||||
|
expect(component.getInnerHTML()).toBe('<h1>false text</h1>'); |
||||
|
}); |
||||
|
|
||||
|
it('should update the ifTrue value using setIfTrue', () => { |
||||
|
const component = cmpRoot.append({ |
||||
|
type: DataConditionType, |
||||
|
condition: { |
||||
|
left: 0, |
||||
|
operator: NumberOperation.greaterThan, |
||||
|
right: -1, |
||||
|
}, |
||||
|
ifTrue: '<h1>some text</h1>', |
||||
|
ifFalse: '<h1>false text</h1>', |
||||
|
})[0] as ComponentDataCondition; |
||||
|
|
||||
|
const newIfTrue = '<h1>new true text</h1>'; |
||||
|
component.setIfTrue(newIfTrue); |
||||
|
expect(component.getIfTrue()).toEqual(newIfTrue); |
||||
|
expect(component.getInnerHTML()).toBe(newIfTrue); |
||||
|
}); |
||||
|
|
||||
|
it('should update the ifFalse value using setIfFalse', () => { |
||||
|
const component = cmpRoot.append({ |
||||
|
type: DataConditionType, |
||||
|
condition: { |
||||
|
left: 0, |
||||
|
operator: NumberOperation.greaterThan, |
||||
|
right: -1, |
||||
|
}, |
||||
|
ifTrue: '<h1>some text</h1>', |
||||
|
ifFalse: '<h1>false text</h1>', |
||||
|
})[0] as ComponentDataCondition; |
||||
|
|
||||
|
const newIfFalse = '<h1>new false text</h1>'; |
||||
|
component.setIfFalse(newIfFalse); |
||||
|
expect(component.getIfFalse()).toEqual(newIfFalse); |
||||
|
|
||||
|
component.setCondition({ |
||||
|
left: 0, |
||||
|
operator: NumberOperation.lessThan, |
||||
|
right: -1, |
||||
|
}); |
||||
|
expect(component.getInnerHTML()).toBe(newIfFalse); |
||||
|
}); |
||||
|
|
||||
|
it('should update the data sources and re-evaluate the condition', () => { |
||||
|
const dataSource = { |
||||
|
id: 'ds1', |
||||
|
records: [ |
||||
|
{ id: 'left_id', left: 'Name1' }, |
||||
|
{ id: 'right_id', right: 'Name1' }, |
||||
|
], |
||||
|
}; |
||||
|
dsm.add(dataSource); |
||||
|
|
||||
|
const component = cmpRoot.append({ |
||||
|
type: DataConditionType, |
||||
|
condition: { |
||||
|
left: { |
||||
|
type: DataVariableType, |
||||
|
path: 'ds1.left_id.left', |
||||
|
}, |
||||
|
operator: AnyTypeOperation.equals, |
||||
|
right: { |
||||
|
type: DataVariableType, |
||||
|
path: 'ds1.right_id.right', |
||||
|
}, |
||||
|
}, |
||||
|
ifTrue: '<h1>True value</h1>', |
||||
|
ifFalse: '<h1>False value</h1>', |
||||
|
})[0] as ComponentDataCondition; |
||||
|
|
||||
|
expect(component.getInnerHTML()).toBe('<h1>True value</h1>'); |
||||
|
|
||||
|
changeDataSourceValue(dsm, 'Different value'); |
||||
|
expect(component.getInnerHTML()).toBe('<h1>False value</h1>'); |
||||
|
|
||||
|
changeDataSourceValue(dsm, 'Name1'); |
||||
|
expect(component.getInnerHTML()).toBe('<h1>True value</h1>'); |
||||
|
}); |
||||
|
|
||||
|
it('should re-render the component when condition, ifTrue, or ifFalse changes', () => { |
||||
|
const component = cmpRoot.append({ |
||||
|
type: DataConditionType, |
||||
|
condition: { |
||||
|
left: 0, |
||||
|
operator: NumberOperation.greaterThan, |
||||
|
right: -1, |
||||
|
}, |
||||
|
ifTrue: '<h1>some text</h1>', |
||||
|
ifFalse: '<h1>false text</h1>', |
||||
|
})[0] as ComponentDataCondition; |
||||
|
|
||||
|
const componentView = component.getView() as ComponentDataConditionView; |
||||
|
|
||||
|
component.setIfTrue('<h1>new true text</h1>'); |
||||
|
expect(componentView.el.innerHTML).toContain('new true text'); |
||||
|
|
||||
|
component.setIfFalse('<h1>new false text</h1>'); |
||||
|
component.setCondition({ |
||||
|
left: 0, |
||||
|
operator: NumberOperation.lessThan, |
||||
|
right: -1, |
||||
|
}); |
||||
|
expect(componentView.el.innerHTML).toContain('new false text'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
function changeDataSourceValue(dsm: DataSourceManager, newValue: string) { |
||||
|
dsm.get('ds1').getRecord('left_id')?.set('left', newValue); |
||||
|
} |
||||
@ -1,150 +1,157 @@ |
|||||
import { |
import { |
||||
GenericOperator, |
AnyTypeOperator, |
||||
GenericOperation, |
AnyTypeOperation, |
||||
} from '../../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; |
} from '../../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; |
||||
|
import Editor from '../../../../../../src/editor/model/Editor'; |
||||
|
import EditorModel from '../../../../../../src/editor/model/Editor'; |
||||
|
|
||||
describe('GenericOperator', () => { |
describe('GenericOperator', () => { |
||||
|
let em: EditorModel; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
em = new Editor(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
em.destroy(); |
||||
|
}); |
||||
|
|
||||
describe('Operator: equals', () => { |
describe('Operator: equals', () => { |
||||
test('should return true when values are equal', () => { |
test('should return true when values are equal', () => { |
||||
const operator = new GenericOperator(GenericOperation.equals); |
const operator = new AnyTypeOperator(AnyTypeOperation.equals, { em }); |
||||
expect(operator.evaluate(5, 5)).toBe(true); |
expect(operator.evaluate(5, 5)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false when values are not equal', () => { |
test('should return false when values are not equal', () => { |
||||
const operator = new GenericOperator(GenericOperation.equals); |
const operator = new AnyTypeOperator(AnyTypeOperation.equals, { em }); |
||||
expect(operator.evaluate(5, 10)).toBe(false); |
expect(operator.evaluate(5, 10)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: isTruthy', () => { |
describe('Operator: isTruthy', () => { |
||||
test('should return true for truthy value', () => { |
test('should return true for truthy value', () => { |
||||
const operator = new GenericOperator(GenericOperation.isTruthy); |
const operator = new AnyTypeOperator(AnyTypeOperation.isTruthy, { em }); |
||||
expect(operator.evaluate('non-empty', null)).toBe(true); |
expect(operator.evaluate('non-empty', null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false for falsy value', () => { |
test('should return false for falsy value', () => { |
||||
const operator = new GenericOperator(GenericOperation.isTruthy); |
const operator = new AnyTypeOperator(AnyTypeOperation.isTruthy, { em }); |
||||
expect(operator.evaluate('', null)).toBe(false); |
expect(operator.evaluate('', null)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: isFalsy', () => { |
describe('Operator: isFalsy', () => { |
||||
test('should return true for falsy value', () => { |
test('should return true for falsy value', () => { |
||||
const operator = new GenericOperator(GenericOperation.isFalsy); |
const operator = new AnyTypeOperator(AnyTypeOperation.isFalsy, { em }); |
||||
expect(operator.evaluate(0, null)).toBe(true); |
expect(operator.evaluate(0, null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false for truthy value', () => { |
test('should return false for truthy value', () => { |
||||
const operator = new GenericOperator(GenericOperation.isFalsy); |
const operator = new AnyTypeOperator(AnyTypeOperation.isFalsy, { em }); |
||||
expect(operator.evaluate(1, null)).toBe(false); |
expect(operator.evaluate(1, null)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: isDefined', () => { |
describe('Operator: isDefined', () => { |
||||
test('should return true for defined value', () => { |
test('should return true for defined value', () => { |
||||
const operator = new GenericOperator(GenericOperation.isDefined); |
const operator = new AnyTypeOperator(AnyTypeOperation.isDefined, { em }); |
||||
expect(operator.evaluate(10, null)).toBe(true); |
expect(operator.evaluate(10, null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false for undefined value', () => { |
test('should return false for undefined value', () => { |
||||
const operator = new GenericOperator(GenericOperation.isDefined); |
const operator = new AnyTypeOperator(AnyTypeOperation.isDefined, { em }); |
||||
expect(operator.evaluate(undefined, null)).toBe(false); |
expect(operator.evaluate(undefined, null)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: isNull', () => { |
describe('Operator: isNull', () => { |
||||
test('should return true for null value', () => { |
test('should return true for null value', () => { |
||||
const operator = new GenericOperator(GenericOperation.isNull); |
const operator = new AnyTypeOperator(AnyTypeOperation.isNull, { em }); |
||||
expect(operator.evaluate(null, null)).toBe(true); |
expect(operator.evaluate(null, null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false for non-null value', () => { |
test('should return false for non-null value', () => { |
||||
const operator = new GenericOperator(GenericOperation.isNull); |
const operator = new AnyTypeOperator(AnyTypeOperation.isNull, { em }); |
||||
expect(operator.evaluate(0, null)).toBe(false); |
expect(operator.evaluate(0, null)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: isUndefined', () => { |
describe('Operator: isUndefined', () => { |
||||
test('should return true for undefined value', () => { |
test('should return true for undefined value', () => { |
||||
const operator = new GenericOperator(GenericOperation.isUndefined); |
const operator = new AnyTypeOperator(AnyTypeOperation.isUndefined, { em }); |
||||
expect(operator.evaluate(undefined, null)).toBe(true); |
expect(operator.evaluate(undefined, null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false for defined value', () => { |
test('should return false for defined value', () => { |
||||
const operator = new GenericOperator(GenericOperation.isUndefined); |
const operator = new AnyTypeOperator(AnyTypeOperation.isUndefined, { em }); |
||||
expect(operator.evaluate(0, null)).toBe(false); |
expect(operator.evaluate(0, null)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: isArray', () => { |
describe('Operator: isArray', () => { |
||||
test('should return true for array', () => { |
test('should return true for array', () => { |
||||
const operator = new GenericOperator(GenericOperation.isArray); |
const operator = new AnyTypeOperator(AnyTypeOperation.isArray, { em }); |
||||
expect(operator.evaluate([1, 2, 3], null)).toBe(true); |
expect(operator.evaluate([1, 2, 3], null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false for non-array', () => { |
test('should return false for non-array', () => { |
||||
const operator = new GenericOperator(GenericOperation.isArray); |
const operator = new AnyTypeOperator(AnyTypeOperation.isArray, { em }); |
||||
expect(operator.evaluate('not an array', null)).toBe(false); |
expect(operator.evaluate('not an array', null)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: isObject', () => { |
describe('Operator: isObject', () => { |
||||
test('should return true for object', () => { |
test('should return true for object', () => { |
||||
const operator = new GenericOperator(GenericOperation.isObject); |
const operator = new AnyTypeOperator(AnyTypeOperation.isObject, { em }); |
||||
expect(operator.evaluate({ key: 'value' }, null)).toBe(true); |
expect(operator.evaluate({ key: 'value' }, null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false for non-object', () => { |
test('should return false for non-object', () => { |
||||
const operator = new GenericOperator(GenericOperation.isObject); |
const operator = new AnyTypeOperator(AnyTypeOperation.isObject, { em }); |
||||
expect(operator.evaluate(42, null)).toBe(false); |
expect(operator.evaluate(42, null)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: isString', () => { |
describe('Operator: isString', () => { |
||||
test('should return true for string', () => { |
test('should return true for string', () => { |
||||
const operator = new GenericOperator(GenericOperation.isString); |
const operator = new AnyTypeOperator(AnyTypeOperation.isString, { em }); |
||||
expect(operator.evaluate('Hello', null)).toBe(true); |
expect(operator.evaluate('Hello', null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false for non-string', () => { |
test('should return false for non-string', () => { |
||||
const operator = new GenericOperator(GenericOperation.isString); |
const operator = new AnyTypeOperator(AnyTypeOperation.isString, { em }); |
||||
expect(operator.evaluate(42, null)).toBe(false); |
expect(operator.evaluate(42, null)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: isNumber', () => { |
describe('Operator: isNumber', () => { |
||||
test('should return true for number', () => { |
test('should return true for number', () => { |
||||
const operator = new GenericOperator(GenericOperation.isNumber); |
const operator = new AnyTypeOperator(AnyTypeOperation.isNumber, { em }); |
||||
expect(operator.evaluate(42, null)).toBe(true); |
expect(operator.evaluate(42, null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false for non-number', () => { |
test('should return false for non-number', () => { |
||||
const operator = new GenericOperator(GenericOperation.isNumber); |
const operator = new AnyTypeOperator(AnyTypeOperation.isNumber, { em }); |
||||
expect(operator.evaluate('not a number', null)).toBe(false); |
expect(operator.evaluate('not a number', null)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: isBoolean', () => { |
describe('Operator: isBoolean', () => { |
||||
test('should return true for boolean', () => { |
test('should return true for boolean', () => { |
||||
const operator = new GenericOperator(GenericOperation.isBoolean); |
const operator = new AnyTypeOperator(AnyTypeOperation.isBoolean, { em }); |
||||
expect(operator.evaluate(true, null)).toBe(true); |
expect(operator.evaluate(true, null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false for non-boolean', () => { |
test('should return false for non-boolean', () => { |
||||
const operator = new GenericOperator(GenericOperation.isBoolean); |
const operator = new AnyTypeOperator(AnyTypeOperation.isBoolean, { em }); |
||||
expect(operator.evaluate(1, null)).toBe(false); |
expect(operator.evaluate(1, null)).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Edge Case Tests', () => { |
describe('Edge Case Tests', () => { |
||||
test('should handle null as input gracefully', () => { |
test('should handle null as input gracefully', () => { |
||||
const operator = new GenericOperator(GenericOperation.isNull); |
const operator = new AnyTypeOperator(AnyTypeOperation.isNull, { em }); |
||||
expect(operator.evaluate(null, null)).toBe(true); |
expect(operator.evaluate(null, null)).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should throw error for unsupported operator', () => { |
|
||||
const operator = new GenericOperator('unsupported' as GenericOperation); |
|
||||
expect(() => operator.evaluate(1, 2)).toThrow('Unsupported generic operator: unsupported'); |
|
||||
}); |
|
||||
}); |
}); |
||||
}); |
}); |
||||
@ -1,59 +1,66 @@ |
|||||
import { |
import { |
||||
LogicalOperator, |
BooleanOperator, |
||||
LogicalOperation, |
BooleanOperation, |
||||
} from '../../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator'; |
} from '../../../../../../src/data_sources/model/conditional_variables/operators/BooleanOperator'; |
||||
|
import Editor from '../../../../../../src/editor/model/Editor'; |
||||
|
import EditorModel from '../../../../../../src/editor/model/Editor'; |
||||
|
|
||||
describe('LogicalOperator', () => { |
describe('LogicalOperator', () => { |
||||
|
let em: EditorModel; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
em = new Editor(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
em.destroy(); |
||||
|
}); |
||||
|
|
||||
describe('Operator: and', () => { |
describe('Operator: and', () => { |
||||
test('should return true when all statements are true', () => { |
test('should return true when all statements are true', () => { |
||||
const operator = new LogicalOperator(LogicalOperation.and); |
const operator = new BooleanOperator(BooleanOperation.and, { em }); |
||||
expect(operator.evaluate([true, true, true])).toBe(true); |
expect(operator.evaluate([true, true, true])).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false when at least one statement is false', () => { |
test('should return false when at least one statement is false', () => { |
||||
const operator = new LogicalOperator(LogicalOperation.and); |
const operator = new BooleanOperator(BooleanOperation.and, { em }); |
||||
expect(operator.evaluate([true, false, true])).toBe(false); |
expect(operator.evaluate([true, false, true])).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: or', () => { |
describe('Operator: or', () => { |
||||
test('should return true when at least one statement is true', () => { |
test('should return true when at least one statement is true', () => { |
||||
const operator = new LogicalOperator(LogicalOperation.or); |
const operator = new BooleanOperator(BooleanOperation.or, { em }); |
||||
expect(operator.evaluate([false, true, false])).toBe(true); |
expect(operator.evaluate([false, true, false])).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false when all statements are false', () => { |
test('should return false when all statements are false', () => { |
||||
const operator = new LogicalOperator(LogicalOperation.or); |
const operator = new BooleanOperator(BooleanOperation.or, { em }); |
||||
expect(operator.evaluate([false, false, false])).toBe(false); |
expect(operator.evaluate([false, false, false])).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Operator: xor', () => { |
describe('Operator: xor', () => { |
||||
test('should return true when exactly one statement is true', () => { |
test('should return true when exactly one statement is true', () => { |
||||
const operator = new LogicalOperator(LogicalOperation.xor); |
const operator = new BooleanOperator(BooleanOperation.xor, { em }); |
||||
expect(operator.evaluate([true, false, false])).toBe(true); |
expect(operator.evaluate([true, false, false])).toBe(true); |
||||
}); |
}); |
||||
|
|
||||
test('should return false when more than one statement is true', () => { |
test('should return false when more than one statement is true', () => { |
||||
const operator = new LogicalOperator(LogicalOperation.xor); |
const operator = new BooleanOperator(BooleanOperation.xor, { em }); |
||||
expect(operator.evaluate([true, true, false])).toBe(false); |
expect(operator.evaluate([true, true, false])).toBe(false); |
||||
}); |
}); |
||||
|
|
||||
test('should return false when no statement is true', () => { |
test('should return false when no statement is true', () => { |
||||
const operator = new LogicalOperator(LogicalOperation.xor); |
const operator = new BooleanOperator(BooleanOperation.xor, { em }); |
||||
expect(operator.evaluate([false, false, false])).toBe(false); |
expect(operator.evaluate([false, false, false])).toBe(false); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|
||||
describe('Edge Case Tests', () => { |
describe('Edge Case Tests', () => { |
||||
test('should return false for xor with all false inputs', () => { |
test('should return false for xor with all false inputs', () => { |
||||
const operator = new LogicalOperator(LogicalOperation.xor); |
const operator = new BooleanOperator(BooleanOperation.xor, { em }); |
||||
expect(operator.evaluate([false, false])).toBe(false); |
expect(operator.evaluate([false, false])).toBe(false); |
||||
}); |
}); |
||||
|
|
||||
test('should throw error for unsupported operator', () => { |
|
||||
const operator = new LogicalOperator('unsupported' as LogicalOperation); |
|
||||
expect(() => operator.evaluate([true, false])).toThrow('Unsupported logical operator: unsupported'); |
|
||||
}); |
|
||||
}); |
}); |
||||
}); |
}); |
||||
@ -0,0 +1,350 @@ |
|||||
|
import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src'; |
||||
|
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; |
||||
|
import { |
||||
|
DataCollectionType, |
||||
|
DataCollectionVariableType, |
||||
|
} from '../../../../../src/data_sources/model/data_collection/constants'; |
||||
|
import { DataCollectionStateVariableType } from '../../../../../src/data_sources/model/data_collection/types'; |
||||
|
import EditorModel from '../../../../../src/editor/model/Editor'; |
||||
|
import { setupTestEditor } from '../../../../common'; |
||||
|
import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection'; |
||||
|
|
||||
|
describe('Collection component getters and setters', () => { |
||||
|
let em: EditorModel; |
||||
|
let dsm: DataSourceManager; |
||||
|
let dataSource: DataSource; |
||||
|
let wrapper: Component; |
||||
|
let firstRecord: DataRecord; |
||||
|
let secondRecord: DataRecord; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
({ em, dsm } = setupTestEditor()); |
||||
|
wrapper = em.getWrapper()!; |
||||
|
dataSource = dsm.add({ |
||||
|
id: 'my_data_source_id', |
||||
|
records: [ |
||||
|
{ id: 'user1', user: 'user1', firstName: 'Name1', age: '12' }, |
||||
|
{ id: 'user2', user: 'user2', firstName: 'Name2', age: '14' }, |
||||
|
{ id: 'user3', user: 'user3', firstName: 'Name3', age: '16' }, |
||||
|
], |
||||
|
}); |
||||
|
firstRecord = dataSource.getRecord('user1')!; |
||||
|
secondRecord = dataSource.getRecord('user2')!; |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
em.destroy(); |
||||
|
}); |
||||
|
|
||||
|
describe('Getters', () => { |
||||
|
let cmp: ComponentDataCollection; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
cmp = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
components: [ |
||||
|
{ |
||||
|
type: 'default', |
||||
|
tagName: 'div', |
||||
|
attributes: { |
||||
|
dataUser: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'my_collection', |
||||
|
startIndex: 1, |
||||
|
endIndex: 2, |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0] as ComponentDataCollection; |
||||
|
}); |
||||
|
|
||||
|
test('getItemsCount should return the correct number of items', () => { |
||||
|
expect(cmp.getItemsCount()).toBe(2); |
||||
|
}); |
||||
|
|
||||
|
test('getConfigStartIndex should return the correct start index', () => { |
||||
|
expect(cmp.getConfigStartIndex()).toBe(1); |
||||
|
}); |
||||
|
|
||||
|
test('getConfigEndIndex should return the correct end index', () => { |
||||
|
expect(cmp.getConfigEndIndex()).toBe(2); |
||||
|
}); |
||||
|
|
||||
|
test('getComponentDef should return the correct component definition', () => { |
||||
|
const componentDef = cmp.getComponentDef(); |
||||
|
|
||||
|
expect(componentDef.type).toBe('default'); |
||||
|
expect(componentDef.components).toHaveLength(1); |
||||
|
expect(componentDef?.components?.[0].attributes?.['dataUser']).toEqual({ |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'user', |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
test('getDataSource should return the correct data source', () => { |
||||
|
const ds = cmp.getDataSource(); |
||||
|
|
||||
|
expect(ds).toEqual({ |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
test('getCollectionId should return the correct collection ID', () => { |
||||
|
expect(cmp.getCollectionId()).toBe('my_collection'); |
||||
|
}); |
||||
|
|
||||
|
test('getItemsCount should return 0 when no records are present', () => { |
||||
|
dataSource.removeRecord('user1'); |
||||
|
dataSource.removeRecord('user2'); |
||||
|
dataSource.removeRecord('user3'); |
||||
|
expect(cmp.getItemsCount()).toBe(0); |
||||
|
}); |
||||
|
|
||||
|
test('getConfigStartIndex should handle zero as a valid start index', () => { |
||||
|
cmp.setStartIndex(0); |
||||
|
|
||||
|
expect(cmp.getConfigStartIndex()).toBe(0); |
||||
|
expect(cmp.getItemsCount()).toBe(3); |
||||
|
}); |
||||
|
|
||||
|
test('getConfigEndIndex should handle zero as a valid end index', () => { |
||||
|
cmp.setEndIndex(0); |
||||
|
|
||||
|
expect(cmp.getConfigStartIndex()).toBe(1); |
||||
|
expect(cmp.getConfigEndIndex()).toBe(0); |
||||
|
expect(cmp.getItemsCount()).toBe(0); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('Setters', () => { |
||||
|
let cmp: ComponentDataCollection; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
cmp = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
components: [ |
||||
|
{ |
||||
|
type: 'default', |
||||
|
tagName: 'div', |
||||
|
attributes: { |
||||
|
dataUser: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'my_collection', |
||||
|
startIndex: 1, |
||||
|
endIndex: 2, |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0] as ComponentDataCollection; |
||||
|
}); |
||||
|
|
||||
|
test('setComponentDef should update the component definition and reflect in children', () => { |
||||
|
const newComponentDef = { |
||||
|
type: 'newType', |
||||
|
components: [ |
||||
|
{ |
||||
|
type: 'default', |
||||
|
tagName: 'span', |
||||
|
attributes: { |
||||
|
'data-name': { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'firstName', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
cmp.setComponentDef(newComponentDef); |
||||
|
|
||||
|
const children = cmp.components(); |
||||
|
expect(children).toHaveLength(2); |
||||
|
expect(children.at(0).get('type')).toBe('newType'); |
||||
|
expect(children.at(0).components().at(0).get('tagName')).toBe('span'); |
||||
|
expect(children.at(0).components().at(0).getAttributes()['data-name']).toBe('Name2'); |
||||
|
}); |
||||
|
|
||||
|
test('setStartIndex should update the start index and reflect in children', () => { |
||||
|
cmp.setStartIndex(0); |
||||
|
expect(cmp.getConfigStartIndex()).toBe(0); |
||||
|
|
||||
|
const children = cmp.components(); |
||||
|
expect(children).toHaveLength(3); |
||||
|
expect(children.at(0).components().at(0).getAttributes()['dataUser']).toBe('user1'); |
||||
|
expect(children.at(1).components().at(0).getAttributes()['dataUser']).toBe('user2'); |
||||
|
expect(children.at(2).components().at(0).getAttributes()['dataUser']).toBe('user3'); |
||||
|
}); |
||||
|
|
||||
|
test('setEndIndex should update the end index and reflect in children', () => { |
||||
|
cmp.setEndIndex(3); |
||||
|
expect(cmp.getConfigEndIndex()).toBe(3); |
||||
|
|
||||
|
const children = cmp.components(); |
||||
|
expect(children).toHaveLength(2); |
||||
|
expect(children.at(0).components().at(0).getAttributes()['dataUser']).toBe('user2'); |
||||
|
expect(children.at(1).components().at(0).getAttributes()['dataUser']).toBe('user3'); |
||||
|
}); |
||||
|
|
||||
|
test('setDataSource should update the data source and reflect in children', () => { |
||||
|
dsm.add({ |
||||
|
id: 'new_data_source_id', |
||||
|
records: [ |
||||
|
{ id: 'user4', user: 'user4', firstName: 'Name4', age: '20' }, |
||||
|
{ id: 'user5', user: 'user5', firstName: 'Name5', age: '21' }, |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
cmp.setDataSource({ |
||||
|
type: DataVariableType, |
||||
|
path: 'new_data_source_id', |
||||
|
}); |
||||
|
|
||||
|
const children = cmp.components(); |
||||
|
expect(children).toHaveLength(1); |
||||
|
expect(children.at(0).components().at(0).getAttributes()['dataUser']).toBe('user5'); |
||||
|
}); |
||||
|
|
||||
|
test('setStartIndex with zero should include the first record', () => { |
||||
|
cmp.setStartIndex(0); |
||||
|
|
||||
|
const children = cmp.components(); |
||||
|
expect(children).toHaveLength(3); |
||||
|
expect(children.at(0).components().at(0).getAttributes()['dataUser']).toBe('user1'); |
||||
|
}); |
||||
|
|
||||
|
test('setEndIndex with zero should result in no children', () => { |
||||
|
cmp.setEndIndex(0); |
||||
|
|
||||
|
const children = cmp.components(); |
||||
|
expect(children).toHaveLength(0); |
||||
|
}); |
||||
|
|
||||
|
test('setDataSource with an empty data source should result in no children', () => { |
||||
|
dsm.add({ |
||||
|
id: 'empty_data_source_id', |
||||
|
records: [], |
||||
|
}); |
||||
|
|
||||
|
cmp.setDataSource({ |
||||
|
type: DataVariableType, |
||||
|
path: 'empty_data_source_id', |
||||
|
}); |
||||
|
|
||||
|
const children = cmp.components(); |
||||
|
expect(children).toHaveLength(0); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('Impact on HTML output', () => { |
||||
|
let cmp: ComponentDataCollection; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
cmp = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
components: [ |
||||
|
{ |
||||
|
type: 'default', |
||||
|
tagName: 'div', |
||||
|
attributes: { |
||||
|
dataUser: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'my_collection', |
||||
|
startIndex: 1, |
||||
|
endIndex: 2, |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0] as ComponentDataCollection; |
||||
|
}); |
||||
|
|
||||
|
test('HTML output should reflect changes in startIndex', () => { |
||||
|
cmp.setStartIndex(0); |
||||
|
|
||||
|
const html = cmp.toHTML(); |
||||
|
expect(html).toContain('dataUser="user1"'); |
||||
|
expect(html).toContain('dataUser="user2"'); |
||||
|
expect(html).toContain('dataUser="user3"'); |
||||
|
}); |
||||
|
|
||||
|
test('HTML output should reflect changes in endIndex', () => { |
||||
|
cmp.setEndIndex(3); |
||||
|
|
||||
|
const html = cmp.toHTML(); |
||||
|
expect(html).toContain('dataUser="user2"'); |
||||
|
expect(html).toContain('dataUser="user3"'); |
||||
|
}); |
||||
|
|
||||
|
test('HTML output should reflect changes in dataSource', () => { |
||||
|
dsm.add({ |
||||
|
id: 'new_data_source_id', |
||||
|
records: [ |
||||
|
{ id: 'user4', user: 'user4', firstName: 'Name4', age: '20' }, |
||||
|
{ id: 'user5', user: 'user5', firstName: 'Name5', age: '21' }, |
||||
|
], |
||||
|
}); |
||||
|
cmp.setDataSource({ |
||||
|
type: DataVariableType, |
||||
|
path: 'new_data_source_id', |
||||
|
}); |
||||
|
|
||||
|
const html = cmp.toHTML(); |
||||
|
expect(html).toContain('dataUser="user5"'); |
||||
|
}); |
||||
|
|
||||
|
test('HTML output should be empty when endIndex is zero', () => { |
||||
|
cmp.setEndIndex(0); |
||||
|
|
||||
|
const html = cmp.toHTML(); |
||||
|
expect(html).not.toContain('dataUser'); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue