Browse Source

Dynamic values improvements (#6419)

* 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
mohamed yahia 11 months ago
committed by GitHub
parent
commit
a4f3d20a42
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 17
      packages/core/src/data_sources/model/ComponentDataVariable.ts
  2. 48
      packages/core/src/data_sources/model/DataResolverListener.ts
  3. 31
      packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts
  4. 5
      packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts
  5. 107
      packages/core/src/data_sources/model/conditional_variables/DataCondition.ts
  6. 93
      packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts
  7. 10
      packages/core/src/data_sources/model/conditional_variables/LogicalGroupEvaluator.ts
  8. 16
      packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts
  9. 14
      packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts
  10. 25
      packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts
  11. 28
      packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts
  12. 13
      packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts
  13. 15
      packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts
  14. 3
      packages/core/src/data_sources/model/conditional_variables/operators/index.ts
  15. 6
      packages/core/src/data_sources/model/conditional_variables/operators/types.ts
  16. 289
      packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts
  17. 1
      packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts
  18. 2
      packages/core/src/data_sources/model/data_collection/types.ts
  19. 5
      packages/core/src/data_sources/model/utils.ts
  20. 2
      packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts
  21. 11
      packages/core/src/dom_components/model/ComponentResolverWatcher.ts
  22. 43
      packages/core/src/domain_abstract/model/StyleableModel.ts
  23. 94
      packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts
  24. 156
      packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts
  25. 36
      packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts
  26. 26
      packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts
  27. 170
      packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts
  28. 69
      packages/core/test/specs/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts
  29. 39
      packages/core/test/specs/data_sources/model/conditional_variables/operators/BooleanOperator.ts
  30. 45
      packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts
  31. 55
      packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts
  32. 350
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts

17
packages/core/src/data_sources/model/ComponentDataVariable.ts

@ -13,6 +13,7 @@ export default class ComponentDataVariable extends Component {
type: DataVariableType, type: DataVariableType,
path: '', path: '',
defaultValue: '', defaultValue: '',
droppable: false,
}; };
} }
@ -22,6 +23,14 @@ export default class ComponentDataVariable extends Component {
this.dataResolver = new DataVariable({ type, path, defaultValue }, opt); this.dataResolver = new DataVariable({ type, path, defaultValue }, opt);
} }
getPath() {
return this.dataResolver.get('path');
}
getDefaultValue() {
return this.dataResolver.get('defaultValue');
}
getDataValue() { getDataValue() {
return this.dataResolver.getDataValue(); return this.dataResolver.getDataValue();
} }
@ -30,6 +39,14 @@ export default class ComponentDataVariable extends Component {
return this.getDataValue(); return this.getDataValue();
} }
setPath(newPath: string) {
this.dataResolver.set('path', newPath);
}
setDefaultValue(newValue: string) {
this.dataResolver.set('defaultValue', newValue);
}
static isComponent(el: HTMLElement) { static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === DataVariableType; return toLowerCase(el.tagName) === DataVariableType;
} }

48
packages/core/src/data_sources/model/DataResolverListener.ts

@ -14,8 +14,12 @@ export interface DataResolverListenerProps {
onUpdate: (value: any) => void; onUpdate: (value: any) => void;
} }
interface ListenerWithCallback extends DataSourceListener {
callback: () => void;
}
export default class DataResolverListener { export default class DataResolverListener {
private listeners: DataSourceListener[] = []; private listeners: ListenerWithCallback[] = [];
private em: EditorModel; private em: EditorModel;
private onUpdate: (value: any) => void; private onUpdate: (value: any) => void;
private model = new Model(); private model = new Model();
@ -33,10 +37,14 @@ export default class DataResolverListener {
this.onUpdate(value); this.onUpdate(value);
}; };
private createListener(obj: any, event: string, callback: () => void = this.onChange): ListenerWithCallback {
return { obj, event, callback };
}
listenToResolver() { listenToResolver() {
const { resolver, model } = this; const { resolver, model } = this;
this.removeListeners(); this.removeListeners();
let listeners: DataSourceListener[] = []; let listeners: ListenerWithCallback[] = [];
const type = resolver.attributes.type; const type = resolver.attributes.type;
switch (type) { switch (type) {
@ -51,11 +59,11 @@ export default class DataResolverListener {
break; break;
} }
listeners.forEach((ls) => model.listenTo(ls.obj, ls.event, this.onChange)); listeners.forEach((ls) => model.listenTo(ls.obj, ls.event, ls.callback));
this.listeners = listeners; this.listeners = listeners;
} }
private listenToConditionalVariable(dataVariable: DataCondition) { private listenToConditionalVariable(dataVariable: DataCondition): ListenerWithCallback[] {
const { em } = this; const { em } = this;
const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => {
return this.listenToDataVariable(new DataVariable(dataVariable, { em })); return this.listenToDataVariable(new DataVariable(dataVariable, { em }));
@ -64,29 +72,41 @@ export default class DataResolverListener {
return dataListeners; return dataListeners;
} }
private listenToDataVariable(dataVariable: DataVariable) { private listenToDataVariable(dataVariable: DataVariable): ListenerWithCallback[] {
const { em } = this; const { em } = this;
const dataListeners: DataSourceListener[] = [];
const { path } = dataVariable.attributes; const { path } = dataVariable.attributes;
const normPath = stringToPath(path || '').join('.'); const normPath = stringToPath(path || '').join('.');
const [ds, dr] = em.DataSources.fromPath(path!); const [ds, dr] = em.DataSources.fromPath(path!);
ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' });
dr && dataListeners.push({ obj: dr, event: 'change' }); const dataListeners: ListenerWithCallback[] = [];
if (ds) {
dataListeners.push(this.createListener(ds.records, 'add remove reset'));
}
if (dr) {
dataListeners.push(this.createListener(dr, 'change'));
}
dataListeners.push( dataListeners.push(
{ obj: dataVariable, event: 'change:path change:defaultValue' }, this.createListener(dataVariable, 'change:path', () => {
{ obj: em.DataSources.all, event: 'add remove reset' }, this.listenToResolver();
{ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }, this.onChange();
}),
this.createListener(dataVariable, 'change:defaultValue'),
this.createListener(em.DataSources.all, 'add remove reset'),
this.createListener(em, `${DataSourcesEvents.path}:${normPath}`),
); );
return dataListeners; return dataListeners;
} }
private listenToDataCollectionVariable(dataVariable: DataCollectionVariable) { private listenToDataCollectionVariable(dataVariable: DataCollectionVariable): ListenerWithCallback[] {
return [{ obj: dataVariable, event: 'change:value' }]; return [this.createListener(dataVariable, 'change:value')];
} }
private removeListeners() { private removeListeners() {
this.listeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange)); this.listeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, ls.callback));
this.listeners = []; this.listeners = [];
} }

31
packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts

@ -2,18 +2,20 @@ import Component from '../../../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types';
import { toLowerCase } from '../../../utils/mixins'; import { toLowerCase } from '../../../utils/mixins';
import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition'; import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition';
import { ConditionProps } from './DataConditionEvaluator';
export default class ComponentDataCondition extends Component { export default class ComponentDataCondition extends Component {
dataResolver: DataCondition; dataResolver: DataCondition;
constructor(props: DataConditionProps, opt: ComponentOptions) { constructor(props: DataConditionProps, opt: ComponentOptions) {
const { condition, ifTrue, ifFalse } = props; const dataConditionInstance = new DataCondition(props, { em: opt.em });
const dataConditionInstance = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em });
super( super(
{ {
...props, ...props,
type: DataConditionType, type: DataConditionType,
components: dataConditionInstance.getDataValue(), components: dataConditionInstance.getDataValue(),
droppable: false,
}, },
opt, opt,
); );
@ -21,8 +23,19 @@ export default class ComponentDataCondition extends Component {
this.dataResolver.onValueChange = this.handleConditionChange.bind(this); this.dataResolver.onValueChange = this.handleConditionChange.bind(this);
} }
getCondition() {
return this.dataResolver.getCondition();
}
getIfTrue() {
return this.dataResolver.getIfTrue();
}
getIfFalse() {
return this.dataResolver.getIfFalse();
}
private handleConditionChange() { private handleConditionChange() {
this.dataResolver.reevaluate();
this.components(this.dataResolver.getDataValue()); this.components(this.dataResolver.getDataValue());
} }
@ -30,6 +43,18 @@ export default class ComponentDataCondition extends Component {
return toLowerCase(el.tagName) === DataConditionType; return toLowerCase(el.tagName) === DataConditionType;
} }
setCondition(newCondition: ConditionProps) {
this.dataResolver.setCondition(newCondition);
}
setIfTrue(newIfTrue: any) {
this.dataResolver.setIfTrue(newIfTrue);
}
setIfFalse(newIfFalse: any) {
this.dataResolver.setIfFalse(newIfFalse);
}
toJSON(): ComponentDefinition { toJSON(): ComponentDefinition {
return this.dataResolver.toJSON(); return this.dataResolver.toJSON();
} }

5
packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts

@ -1,9 +1,10 @@
import { Operator } from './operators'; import { Operator } from './operators/BaseOperator';
import { DataConditionOperation } from './operators/types';
export class ConditionStatement { export class ConditionStatement {
constructor( constructor(
private leftValue: any, private leftValue: any,
private operator: Operator, private operator: Operator<DataConditionOperation>,
private rightValue: any, private rightValue: any,
) {} ) {}

107
packages/core/src/data_sources/model/conditional_variables/DataCondition.ts

@ -3,22 +3,23 @@ import EditorModel from '../../../editor/model/Editor';
import DataVariable, { DataVariableProps } from '../DataVariable'; import DataVariable, { DataVariableProps } from '../DataVariable';
import DataResolverListener from '../DataResolverListener'; import DataResolverListener from '../DataResolverListener';
import { evaluateVariable, isDataVariable } from '../utils'; import { evaluateVariable, isDataVariable } from '../utils';
import { Condition, ConditionProps } from './Condition'; import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator';
import { GenericOperation } from './operators/GenericOperator'; import { AnyTypeOperation } from './operators/AnyTypeOperator';
import { LogicalOperation } from './operators/LogicalOperator'; import { BooleanOperation } from './operators/BooleanOperator';
import { NumberOperation } from './operators/NumberOperator'; import { NumberOperation } from './operators/NumberOperator';
import { StringOperation } from './operators/StringOperations'; import { StringOperation } from './operators/StringOperator';
import { isUndefined } from 'underscore';
export const DataConditionType = 'data-condition'; export const DataConditionType = 'data-condition';
export interface ExpressionProps { export interface ExpressionProps {
left: any; left: any;
operator: GenericOperation | StringOperation | NumberOperation; operator: AnyTypeOperation | StringOperation | NumberOperation;
right: any; right: any;
} }
export interface LogicGroupProps { export interface LogicGroupProps {
logicalOperator: LogicalOperation; logicalOperator: BooleanOperation;
statements: ConditionProps[]; statements: ConditionProps[];
} }
@ -30,57 +31,90 @@ export interface DataConditionProps {
} }
interface DataConditionPropsDefined extends Omit<DataConditionProps, 'condition'> { interface DataConditionPropsDefined extends Omit<DataConditionProps, 'condition'> {
condition: Condition; condition: DataConditionEvaluator;
} }
export class DataCondition extends Model<DataConditionPropsDefined> { export class DataCondition extends Model<DataConditionPropsDefined> {
lastEvaluationResult: boolean;
private em: EditorModel; private em: EditorModel;
private resolverListeners: DataResolverListener[] = []; private resolverListeners: DataResolverListener[] = [];
private _onValueChange?: () => void; private _onValueChange?: () => void;
constructor( constructor(
condition: ConditionProps, props: {
public ifTrue: any, condition: ConditionProps;
public ifFalse: any, ifTrue: any;
ifFalse: any;
},
opts: { em: EditorModel; onValueChange?: () => void }, opts: { em: EditorModel; onValueChange?: () => void },
) { ) {
if (typeof condition === 'undefined') { if (isUndefined(props.condition)) {
throw new MissingConditionError(); opts.em.logError('No condition was provided to a conditional component.');
} }
const conditionInstance = new Condition(condition, { em: opts.em }); const conditionInstance = new DataConditionEvaluator({ condition: props.condition }, { em: opts.em });
super({ super({
type: DataConditionType, type: DataConditionType,
...props,
condition: conditionInstance, condition: conditionInstance,
ifTrue,
ifFalse,
}); });
this.em = opts.em; this.em = opts.em;
this.lastEvaluationResult = this.evaluate();
this.listenToDataVariables(); this.listenToDataVariables();
this._onValueChange = opts.onValueChange; this._onValueChange = opts.onValueChange;
this.on('change:condition change:ifTrue change:ifFalse', () => {
this.listenToDataVariables();
this._onValueChange?.();
});
} }
get condition() { private get conditionEvaluator() {
return this.get('condition')!; return this.get('condition')!;
} }
evaluate() { getCondition(): ConditionProps {
return this.condition.evaluate(); return this.get('condition')?.get('condition')!;
}
getIfTrue() {
return this.get('ifTrue')!;
}
getIfFalse() {
return this.get('ifFalse')!;
} }
getDataValue(): any { isTrue(): boolean {
return this.lastEvaluationResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em); return this.conditionEvaluator.evaluate();
} }
reevaluate(): void { getDataValue(skipDynamicValueResolution: boolean = false): any {
this.lastEvaluationResult = this.evaluate(); const ifTrue = this.get('ifTrue');
const ifFalse = this.get('ifFalse');
const isConditionTrue = this.isTrue();
if (skipDynamicValueResolution) {
return isConditionTrue ? ifTrue : ifFalse;
}
return isConditionTrue ? evaluateVariable(ifTrue, this.em) : evaluateVariable(ifFalse, this.em);
} }
set onValueChange(newFunction: () => void) { set onValueChange(newFunction: () => void) {
this._onValueChange = newFunction; this._onValueChange = newFunction;
this.listenToDataVariables(); }
setCondition(newCondition: ConditionProps) {
const newConditionInstance = new DataConditionEvaluator({ condition: newCondition }, { em: this.em });
this.set('condition', newConditionInstance);
}
setIfTrue(newIfTrue: any) {
this.set('ifTrue', newIfTrue);
}
setIfFalse(newIfFalse: any) {
this.set('ifFalse', newIfFalse);
} }
private listenToDataVariables() { private listenToDataVariables() {
@ -97,7 +131,6 @@ export class DataCondition extends Model<DataConditionPropsDefined> {
em, em,
resolver: new DataVariable(variable, { em: this.em }), resolver: new DataVariable(variable, { em: this.em }),
onUpdate: (() => { onUpdate: (() => {
this.reevaluate();
this._onValueChange?.(); this._onValueChange?.();
}).bind(this), }).bind(this),
}); });
@ -107,9 +140,11 @@ export class DataCondition extends Model<DataConditionPropsDefined> {
} }
getDependentDataVariables() { getDependentDataVariables() {
const dataVariables: DataVariableProps[] = this.condition.getDataVariables(); const dataVariables: DataVariableProps[] = this.conditionEvaluator.getDependentDataVariables();
if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); const ifTrue = this.get('ifTrue');
if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); const ifFalse = this.get('ifFalse');
if (isDataVariable(ifTrue)) dataVariables.push(ifTrue);
if (isDataVariable(ifFalse)) dataVariables.push(ifFalse);
return dataVariables; return dataVariables;
} }
@ -120,16 +155,14 @@ export class DataCondition extends Model<DataConditionPropsDefined> {
} }
toJSON() { toJSON() {
const ifTrue = this.get('ifTrue');
const ifFalse = this.get('ifFalse');
return { return {
type: DataConditionType, type: DataConditionType,
condition: this.condition, condition: this.conditionEvaluator,
ifTrue: this.ifTrue, ifTrue,
ifFalse: this.ifFalse, ifFalse,
}; };
} }
} }
export class MissingConditionError extends Error {
constructor() {
super('No condition was provided to a conditional component.');
}
}

93
packages/core/src/data_sources/model/conditional_variables/Condition.ts → packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts

@ -1,41 +1,39 @@
import { DataVariableProps } from './../DataVariable'; import { DataVariableProps } from '../DataVariable';
import EditorModel from '../../../editor/model/Editor'; import EditorModel from '../../../editor/model/Editor';
import { evaluateVariable, isDataVariable } from '../utils'; import { evaluateVariable, isDataVariable } from '../utils';
import { ExpressionProps, LogicGroupProps } from './DataCondition'; import { ExpressionProps, LogicGroupProps } from './DataCondition';
import { LogicalGroupStatement } from './LogicalGroupStatement'; import { LogicalGroupEvaluator } from './LogicalGroupEvaluator';
import { Operator } from './operators'; import { Operator } from './operators/BaseOperator';
import { GenericOperation, GenericOperator } from './operators/GenericOperator'; import { AnyTypeOperation, AnyTypeOperator } from './operators/AnyTypeOperator';
import { LogicalOperator } from './operators/LogicalOperator'; import { BooleanOperator } from './operators/BooleanOperator';
import { NumberOperator, NumberOperation } from './operators/NumberOperator'; import { NumberOperator, NumberOperation } from './operators/NumberOperator';
import { StringOperator, StringOperation } from './operators/StringOperations'; import { StringOperator, StringOperation } from './operators/StringOperator';
import { Model } from '../../../common'; import { Model } from '../../../common';
import { DataConditionOperation } from './operators/types';
export type ConditionProps = ExpressionProps | LogicGroupProps | boolean; export type ConditionProps = ExpressionProps | LogicGroupProps | boolean;
export class Condition extends Model { interface DataConditionEvaluatorProps {
private condition: ConditionProps; condition: ConditionProps;
}
export class DataConditionEvaluator extends Model<DataConditionEvaluatorProps> {
private em: EditorModel; private em: EditorModel;
constructor(props: ConditionProps, opts: { em: EditorModel }) { constructor(props: DataConditionEvaluatorProps, opts: { em: EditorModel }) {
super(props); super(props);
this.condition = props;
this.em = opts.em; this.em = opts.em;
} }
evaluate(): boolean { evaluate(): boolean {
return this.evaluateCondition(this.condition); const em = this.em;
} const condition = this.get('condition');
/**
* Recursively evaluates conditions and logic groups.
*/
private evaluateCondition(condition: ConditionProps): boolean {
if (typeof condition === 'boolean') return condition; if (typeof condition === 'boolean') return condition;
if (this.isLogicGroup(condition)) { if (this.isLogicGroup(condition)) {
const { logicalOperator, statements } = condition; const { logicalOperator, statements } = condition;
const operator = new LogicalOperator(logicalOperator); const operator = new BooleanOperator(logicalOperator, { em });
const logicalGroup = new LogicalGroupStatement(operator, statements, { em: this.em }); const logicalGroup = new LogicalGroupEvaluator(operator, statements, { em });
return logicalGroup.evaluate(); return logicalGroup.evaluate();
} }
@ -49,62 +47,65 @@ export class Condition extends Model {
return evaluated; return evaluated;
} }
throw new Error('Invalid condition type.'); this.em.logError('Invalid condition type.');
return false;
} }
/** /**
* Factory method for creating operators based on the data type. * Factory method for creating operators based on the data type.
*/ */
private getOperator(left: any, operator: string): Operator { private getOperator(left: any, operator: string): Operator<DataConditionOperation> {
if (this.isOperatorInEnum(operator, GenericOperation)) { const em = this.em;
return new GenericOperator(operator as GenericOperation);
if (this.isOperatorInEnum(operator, AnyTypeOperation)) {
return new AnyTypeOperator(operator as AnyTypeOperation, { em });
} else if (typeof left === 'number') { } else if (typeof left === 'number') {
return new NumberOperator(operator as NumberOperation); return new NumberOperator(operator as NumberOperation, { em });
} else if (typeof left === 'string') { } else if (typeof left === 'string') {
return new StringOperator(operator as StringOperation); return new StringOperator(operator as StringOperation, { em });
} }
throw new Error(`Unsupported data type: ${typeof left}`); throw new Error(`Unsupported data type: ${typeof left}`);
} }
/** getDependentDataVariables(): DataVariableProps[] {
* Extracts all data variables from the condition, including nested ones. const condition = this.get('condition');
*/ if (!condition) return [];
getDataVariables() {
const variables: DataVariableProps[] = []; return this.extractDataVariables(condition);
this.extractVariables(this.condition, variables);
return variables;
} }
/** private extractDataVariables(condition: ConditionProps): DataVariableProps[] {
* Recursively extracts variables from expressions or logic groups. const variables: DataVariableProps[] = [];
*/
private extractVariables(condition: ConditionProps, variables: DataVariableProps[]): void {
if (this.isExpression(condition)) { if (this.isExpression(condition)) {
if (isDataVariable(condition.left)) variables.push(condition.left); if (isDataVariable(condition.left)) variables.push(condition.left);
if (isDataVariable(condition.right)) variables.push(condition.right); if (isDataVariable(condition.right)) variables.push(condition.right);
} else if (this.isLogicGroup(condition)) { } else if (this.isLogicGroup(condition)) {
condition.statements.forEach((stmt) => this.extractVariables(stmt, variables)); condition.statements.forEach((stmt) => variables.push(...this.extractDataVariables(stmt)));
} }
return variables;
} }
/**
* Checks if a condition is a LogicGroup.
*/
private isLogicGroup(condition: any): condition is LogicGroupProps { private isLogicGroup(condition: any): condition is LogicGroupProps {
return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements); return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements);
} }
/**
* Checks if a condition is an Expression.
*/
private isExpression(condition: any): condition is ExpressionProps { private isExpression(condition: any): condition is ExpressionProps {
return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string';
} }
/**
* Checks if an operator exists in a specific enum.
*/
private isOperatorInEnum(operator: string, enumObject: any): boolean { private isOperatorInEnum(operator: string, enumObject: any): boolean {
return Object.values(enumObject).includes(operator); return Object.values(enumObject).includes(operator);
} }
toJSON(options?: any) {
const condition = this.get('condition');
if (typeof condition === 'object') {
const json = JSON.parse(JSON.stringify(condition));
return json;
}
return condition;
}
} }

10
packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts → packages/core/src/data_sources/model/conditional_variables/LogicalGroupEvaluator.ts

@ -1,12 +1,12 @@
import EditorModel from '../../../editor/model/Editor'; import EditorModel from '../../../editor/model/Editor';
import { Condition, ConditionProps } from './Condition'; import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator';
import { LogicalOperator } from './operators/LogicalOperator'; import { BooleanOperator } from './operators/BooleanOperator';
export class LogicalGroupStatement { export class LogicalGroupEvaluator {
private em: EditorModel; private em: EditorModel;
constructor( constructor(
private operator: LogicalOperator, private operator: BooleanOperator,
private statements: ConditionProps[], private statements: ConditionProps[],
opts: { em: EditorModel }, opts: { em: EditorModel },
) { ) {
@ -15,7 +15,7 @@ export class LogicalGroupStatement {
evaluate(): boolean { evaluate(): boolean {
const results = this.statements.map((statement) => { const results = this.statements.map((statement) => {
const condition = new Condition(statement, { em: this.em }); const condition = new DataConditionEvaluator({ condition: statement }, { em: this.em });
return condition.evaluate(); return condition.evaluate();
}); });
return this.operator.evaluate(results); return this.operator.evaluate(results);

16
packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts → packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts

@ -1,7 +1,8 @@
import DataVariable from '../../DataVariable'; import DataVariable from '../../DataVariable';
import { Operator } from '.'; import { Operator } from './BaseOperator';
import EditorModel from '../../../../editor/model/Editor';
export enum GenericOperation { export enum AnyTypeOperation {
equals = 'equals', equals = 'equals',
isTruthy = 'isTruthy', isTruthy = 'isTruthy',
isFalsy = 'isFalsy', isFalsy = 'isFalsy',
@ -16,13 +17,9 @@ export enum GenericOperation {
isDefaultValue = 'isDefaultValue', // For Datasource variables isDefaultValue = 'isDefaultValue', // For Datasource variables
} }
export class GenericOperator extends Operator { export class AnyTypeOperator extends Operator<AnyTypeOperation> {
constructor(private operator: GenericOperation) {
super();
}
evaluate(left: any, right: any): boolean { evaluate(left: any, right: any): boolean {
switch (this.operator) { switch (this.operation) {
case 'equals': case 'equals':
return left === right; return left === right;
case 'isTruthy': case 'isTruthy':
@ -48,7 +45,8 @@ export class GenericOperator extends Operator {
case 'isDefaultValue': case 'isDefaultValue':
return left instanceof DataVariable && left.get('defaultValue') === right; return left instanceof DataVariable && left.get('defaultValue') === right;
default: default:
throw new Error(`Unsupported generic operator: ${this.operator}`); this.em?.logError(`Unsupported generic operation: ${this.operation}`);
return false;
} }
} }
} }

14
packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts

@ -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;
}

25
packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts

@ -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;
}
}
}

28
packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts

@ -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}`);
}
}
}

13
packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts

@ -1,4 +1,4 @@
import { Operator } from '.'; import { Operator } from './BaseOperator';
export enum NumberOperation { export enum NumberOperation {
greaterThan = '>', greaterThan = '>',
@ -9,13 +9,9 @@ export enum NumberOperation {
notEquals = '!=', notEquals = '!=',
} }
export class NumberOperator extends Operator { export class NumberOperator extends Operator<NumberOperation> {
constructor(private operator: NumberOperation) {
super();
}
evaluate(left: number, right: number): boolean { evaluate(left: number, right: number): boolean {
switch (this.operator) { switch (this.operation) {
case NumberOperation.greaterThan: case NumberOperation.greaterThan:
return left > right; return left > right;
case NumberOperation.lessThan: case NumberOperation.lessThan:
@ -29,7 +25,8 @@ export class NumberOperator extends Operator {
case NumberOperation.notEquals: case NumberOperation.notEquals:
return left !== right; return left !== right;
default: default:
throw new Error(`Unsupported number operator: ${this.operator}`); this.em.logError(`Unsupported number operation: ${this.operation}`);
return false;
} }
} }
} }

15
packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts → packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts

@ -1,4 +1,4 @@
import { Operator } from '.'; import { Operator } from './BaseOperator';
export enum StringOperation { export enum StringOperation {
contains = 'contains', contains = 'contains',
@ -9,13 +9,9 @@ export enum StringOperation {
trimEquals = 'trimEquals', trimEquals = 'trimEquals',
} }
export class StringOperator extends Operator { export class StringOperator extends Operator<StringOperation> {
constructor(private operator: StringOperation) {
super();
}
evaluate(left: string, right: string) { evaluate(left: string, right: string) {
switch (this.operator) { switch (this.operation) {
case StringOperation.contains: case StringOperation.contains:
return left.includes(right); return left.includes(right);
case StringOperation.startsWith: case StringOperation.startsWith:
@ -23,14 +19,15 @@ export class StringOperator extends Operator {
case StringOperation.endsWith: case StringOperation.endsWith:
return left.endsWith(right); return left.endsWith(right);
case StringOperation.matchesRegex: case StringOperation.matchesRegex:
if (!right) throw new Error('Regex pattern must be provided.'); if (!right) this.em.logError('Regex pattern must be provided.');
return new RegExp(right).test(left); return new RegExp(right).test(left);
case StringOperation.equalsIgnoreCase: case StringOperation.equalsIgnoreCase:
return left.toLowerCase() === right.toLowerCase(); return left.toLowerCase() === right.toLowerCase();
case StringOperation.trimEquals: case StringOperation.trimEquals:
return left.trim() === right.trim(); return left.trim() === right.trim();
default: default:
throw new Error(`Unsupported string operator: ${this.operator}`); this.em.logError(`Unsupported string operation: ${this.operation}`);
return false;
} }
} }
} }

3
packages/core/src/data_sources/model/conditional_variables/operators/index.ts

@ -1,3 +0,0 @@
export abstract class Operator {
abstract evaluate(left: any, right: any): boolean;
}

6
packages/core/src/data_sources/model/conditional_variables/operators/types.ts

@ -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;

289
packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts

@ -1,4 +1,4 @@
import { isArray } from 'underscore'; import { bindAll, isArray } from 'underscore';
import { ObjectAny } from '../../../common'; import { ObjectAny } from '../../../common';
import Component from '../../../dom_components/model/Component'; import Component from '../../../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types';
@ -17,12 +17,16 @@ import {
DataCollectionState, DataCollectionState,
DataCollectionStateMap, DataCollectionStateMap,
} from './types'; } from './types';
import { getSymbolsToUpdate } from '../../../dom_components/model/SymbolUtils';
import { StyleProps, UpdateStyleOptions } from '../../../domain_abstract/model/StyleableModel';
import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers'; import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers';
export default class ComponentDataCollection extends Component { export default class ComponentDataCollection extends Component {
dataSourceWatcher?: DataResolverListener;
constructor(props: ComponentDataCollectionProps, opt: ComponentOptions) { constructor(props: ComponentDataCollectionProps, opt: ComponentOptions) {
const collectionDef = props[keyCollectionDefinition]; const collectionDef = props[keyCollectionDefinition];
// If we are cloning, leave setting the collection items to the main symbol collection
if (opt.forCloning) { if (opt.forCloning) {
return super(props as any, opt) as unknown as ComponentDataCollection; return super(props as any, opt) as unknown as ComponentDataCollection;
} }
@ -36,129 +40,202 @@ export default class ComponentDataCollection extends Component {
return cmp; return cmp;
} }
const parentCollectionStateMap = (props[keyCollectionsStateMap] || {}) as DataCollectionStateMap; this.rebuildChildrenFromCollection();
const components: Component[] = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt); bindAll(this, 'rebuildChildrenFromCollection');
cmp.components(components, opt); this.listenTo(this, `change:${keyCollectionDefinition}`, this.rebuildChildrenFromCollection);
this.listenToDataSource();
return cmp;
}
getItemsCount() {
const items = this.getDataSourceItems();
const startIndex = Math.max(0, this.getConfigStartIndex() ?? 0);
const configEndIndex = this.getConfigEndIndex() ?? Number.MAX_VALUE;
const endIndex = Math.min(items.length - 1, configEndIndex);
const count = endIndex - startIndex + 1;
return Math.max(0, count);
}
getConfigStartIndex() {
return this.collectionConfig.startIndex;
}
getConfigEndIndex() {
return this.collectionConfig.endIndex;
}
getComponentDef(): ComponentDefinition {
return this.getFirstChildJSON();
}
getDataSource(): DataCollectionDataSource {
return this.collectionDef?.collectionConfig?.dataSource;
}
getCollectionId(): string {
return this.collectionDef?.collectionConfig?.collectionId;
}
setComponentDef(componentDef: ComponentDefinition) {
this.set(keyCollectionDefinition, { ...this.collectionDef, componentDef });
}
if (isDataVariable(this.collectionDataSource)) { setStartIndex(startIndex: number): void {
this.watchDataSource(parentCollectionStateMap, opt); if (startIndex < 0) {
this.em.logError('Start index should be greater than or equal to 0');
return;
} }
return cmp; this.updateCollectionConfig({ startIndex });
} }
get collectionConfig() { setEndIndex(endIndex: number): void {
return this.get(keyCollectionDefinition).collectionConfig as DataCollectionConfig; this.updateCollectionConfig({ endIndex });
} }
get collectionDataSource() { private updateCollectionConfig(updates: Partial<DataCollectionConfig>): void {
return this.collectionConfig.dataSource; this.set(keyCollectionDefinition, {
...this.collectionDef,
collectionConfig: {
...this.collectionConfig,
...updates,
},
});
} }
toJSON(opts?: ObjectAny) { setDataSource(dataSource: DataCollectionDataSource) {
const json = super.toJSON.call(this, opts) as ComponentDataCollectionProps; this.set(keyCollectionDefinition, {
json[keyCollectionDefinition].componentDef = this.getComponentDef(); ...this.collectionDef,
delete json.components; collectionConfig: { ...this.collectionConfig, dataSource },
delete json.droppable; });
return json; }
private getDataSourceItems() {
return this.collectionDef?.collectionConfig ? getDataSourceItems(this.collectionConfig.dataSource, this.em) : [];
}
private getCollectionStateMap() {
return (this.get(keyCollectionsStateMap) || {}) as DataCollectionStateMap;
}
private get collectionDef() {
return (this.get(keyCollectionDefinition) || {}) as DataCollectionProps;
}
private get collectionConfig() {
return (this.collectionDef?.collectionConfig || {}) as DataCollectionConfig;
}
private get collectionDataSource() {
return this.collectionConfig.dataSource;
} }
private getComponentDef() { private getFirstChildJSON() {
const firstChild = this.components().at(0); const firstChild = this.components().at(0);
const firstChildJSON = firstChild ? serialize(firstChild) : this.get(keyCollectionDefinition).componentDef; const firstChildJSON = firstChild ? serialize(firstChild) : this.collectionDef.componentDef;
delete firstChildJSON?.draggable; delete firstChildJSON?.draggable;
delete firstChildJSON?.removable;
return firstChildJSON; return firstChildJSON;
} }
private watchDataSource(parentCollectionStateMap: DataCollectionStateMap, opt: ComponentOptions) { private listenToDataSource() {
const { em } = this; const { em } = this;
const path = this.collectionDataSource?.path; const path = this.collectionDataSource?.path;
if (!path) return; if (!path) return;
this.dataSourceWatcher = new DataResolverListener({
new DataResolverListener({
em, em,
resolver: new DataVariable({ type: DataVariableType, path }, { em }), resolver: new DataVariable({ type: DataVariableType, path }, { em }),
onUpdate: () => { onUpdate: this.rebuildChildrenFromCollection,
const collectionDef = { ...this.get(keyCollectionDefinition), componentDef: this.getComponentDef() };
const collectionItems = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt);
this.components().reset(collectionItems, updateFromWatcher as any);
},
}); });
} }
static isComponent(el: HTMLElement) { private rebuildChildrenFromCollection() {
return toLowerCase(el.tagName) === DataCollectionType; this.components().reset(this.getCollectionItems(), updateFromWatcher as any);
}
}
function getCollectionItems(
em: EditorModel,
collectionDef: DataCollectionProps,
parentCollectionStateMap: DataCollectionStateMap,
opt: ComponentOptions,
) {
const { componentDef, collectionConfig } = collectionDef;
const result = validateCollectionConfig(collectionConfig, componentDef, em);
if (!result) {
return [];
} }
const components: Component[] = []; getCollectionItems() {
const collectionId = collectionConfig.collectionId; const { componentDef, collectionConfig } = this.collectionDef;
const items = getDataSourceItems(collectionConfig.dataSource, em); const result = validateCollectionConfig(collectionConfig, componentDef, this.em);
const startIndex = Math.max(0, collectionConfig.startIndex || 0);
const endIndex = Math.min(
items.length - 1,
collectionConfig.endIndex !== undefined ? collectionConfig.endIndex : Number.MAX_VALUE,
);
const totalItems = endIndex - startIndex + 1;
let symbolMain: Component;
for (let index = startIndex; index <= endIndex; index++) {
const item = items[index];
const collectionState: DataCollectionState = {
collectionId,
currentIndex: index,
currentItem: item,
startIndex: startIndex,
endIndex: endIndex,
totalItems: totalItems,
remainingItems: totalItems - (index + 1),
};
if (parentCollectionStateMap[collectionId]) { if (!result) {
em.logError(
`The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`,
);
return []; return [];
} }
const collectionsStateMap: DataCollectionStateMap = { const components: Component[] = [];
...parentCollectionStateMap, const collectionId = collectionConfig.collectionId;
[collectionId]: collectionState, const items = this.getDataSourceItems();
};
const startIndex = this.getConfigStartIndex() ?? 0;
const configEndIndex = this.getConfigEndIndex() ?? Number.MAX_VALUE;
const endIndex = Math.min(items.length - 1, configEndIndex);
const totalItems = endIndex - startIndex + 1;
const parentCollectionStateMap = this.getCollectionStateMap();
let symbolMain: Component;
for (let index = startIndex; index <= endIndex; index++) {
const item = items[index];
const isFirstItem = index === startIndex;
const collectionState: DataCollectionState = {
collectionId,
currentIndex: index,
currentItem: item,
startIndex: startIndex,
endIndex: endIndex,
totalItems: totalItems,
remainingItems: totalItems - (index + 1),
};
if (parentCollectionStateMap[collectionId]) {
this.em.logError(
`The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`,
);
return [];
}
if (index === startIndex) { const collectionsStateMap: DataCollectionStateMap = {
const componentType = (componentDef?.type as string) || 'default'; ...parentCollectionStateMap,
let type = em.Components.getType(componentType) || em.Components.getType('default'); [collectionId]: collectionState,
const Model = type.model; };
symbolMain = new Model({ ...serialize(componentDef), draggable: false }, opt);
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(symbolMain); if (isFirstItem) {
const componentType = (componentDef?.type as string) || 'default';
let type = this.em.Components.getType(componentType) || this.em.Components.getType('default');
const Model = type.model;
symbolMain = new Model(
{
...serialize(componentDef),
draggable: false,
removable: false,
},
this.opt,
);
}
const instance = symbolMain!.clone({ symbol: true });
!isFirstItem && instance.set('locked', true);
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance);
components.push(instance);
} }
const instance = symbolMain!.clone({ symbol: true }); return components;
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance); }
components.push(instance); static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === DataCollectionType;
} }
return components; toJSON(opts?: ObjectAny) {
const json = super.toJSON.call(this, opts) as ComponentDataCollectionProps;
json[keyCollectionDefinition].componentDef = this.getFirstChildJSON();
delete json.components;
delete json.droppable;
return json;
}
} }
function setCollectionStateMapAndPropagate( function setCollectionStateMapAndPropagate(collectionsStateMap: DataCollectionStateMap, collectionId: string) {
collectionsStateMap: DataCollectionStateMap,
collectionId: string | undefined,
) {
return (cmp: Component) => { return (cmp: Component) => {
setCollectionStateMap(collectionsStateMap)(cmp); setCollectionStateMap(collectionsStateMap)(cmp);
@ -169,7 +246,6 @@ function setCollectionStateMapAndPropagate(
const listenerKey = `_hasAddListener${collectionId ? `_${collectionId}` : ''}`; const listenerKey = `_hasAddListener${collectionId ? `_${collectionId}` : ''}`;
const cmps = cmp.components(); const cmps = cmp.components();
// Add the 'add' listener if not already in the listeners array
if (!cmp.collectionStateListeners.includes(listenerKey)) { if (!cmp.collectionStateListeners.includes(listenerKey)) {
cmp.listenTo(cmps, 'add', addListener); cmp.listenTo(cmps, 'add', addListener);
cmp.collectionStateListeners.push(listenerKey); cmp.collectionStateListeners.push(listenerKey);
@ -178,9 +254,15 @@ function setCollectionStateMapAndPropagate(
component.stopListening(component.components(), 'add', addListener); component.stopListening(component.components(), 'add', addListener);
component.off(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); component.off(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange);
const index = component.collectionStateListeners.indexOf(listenerKey); const index = component.collectionStateListeners.indexOf(listenerKey);
if (index > -1) { if (index !== -1) {
component.collectionStateListeners.splice(index, 1); component.collectionStateListeners.splice(index, 1);
} }
const collectionsStateMap = component.get(keyCollectionsStateMap);
component.set(keyCollectionsStateMap, {
...collectionsStateMap,
[collectionId]: undefined,
});
}; };
cmp.listenTo(cmps, 'remove', removeListener); cmp.listenTo(cmps, 'remove', removeListener);
@ -229,6 +311,12 @@ function validateCollectionConfig(
} }
} }
const startIndex = collectionConfig?.startIndex;
if (startIndex !== undefined && (startIndex < 0 || !Number.isInteger(startIndex))) {
em.logError(`Invalid startIndex: ${startIndex}. It must be a non-negative integer.`);
}
return true; return true;
} }
@ -241,6 +329,28 @@ function setCollectionStateMap(collectionsStateMap: DataCollectionStateMap) {
}; };
cmp.set(keyCollectionsStateMap, updatedCollectionStateMap); cmp.set(keyCollectionsStateMap, updatedCollectionStateMap);
cmp.dataResolverWatchers.updateCollectionStateMap(updatedCollectionStateMap); cmp.dataResolverWatchers.updateCollectionStateMap(updatedCollectionStateMap);
const parentCollectionsId = Object.keys(updatedCollectionStateMap);
const isFirstItem = parentCollectionsId.every(
(key) => updatedCollectionStateMap[key].currentIndex === updatedCollectionStateMap[key].startIndex,
);
if (isFirstItem) {
const __onStyleChange = cmp.__onStyleChange.bind(cmp);
cmp.__onStyleChange = (newStyles: StyleProps, opts: UpdateStyleOptions = {}) => {
__onStyleChange(newStyles);
const cmps = getSymbolsToUpdate(cmp);
cmps.forEach((cmp) => {
cmp.addStyle(newStyles, opts);
});
};
cmp.on(`change:${keyIsCollectionItem}`, () => {
cmp.__onStyleChange = __onStyleChange;
});
}
}; };
} }
@ -262,7 +372,6 @@ function getDataSourceItems(dataSource: DataCollectionDataSource, em: EditorMode
const id = dataSource.path; const id = dataSource.path;
items = listDataSourceVariables(id, em); items = listDataSourceVariables(id, em);
} else { } else {
// Path points to a record in the data source
items = em.DataSources.getValue(dataSource.path, []); items = em.DataSources.getValue(dataSource.path, []);
} }
break; break;

1
packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts

@ -18,6 +18,7 @@ export default class ComponentDataCollectionVariable extends Component {
collectionId: undefined, collectionId: undefined,
variableType: undefined, variableType: undefined,
path: undefined, path: undefined,
droppable: false,
}; };
} }

2
packages/core/src/data_sources/model/data_collection/types.ts

@ -36,6 +36,7 @@ export interface DataCollectionStateMap {
} }
export interface ComponentDataCollectionProps extends ComponentDefinition { export interface ComponentDataCollectionProps extends ComponentDefinition {
type: typeof DataCollectionType;
[keyCollectionDefinition]: DataCollectionProps; [keyCollectionDefinition]: DataCollectionProps;
} }
@ -44,7 +45,6 @@ export interface ComponentDataCollectionVariableProps
Omit<ComponentProperties, 'type'> {} Omit<ComponentProperties, 'type'> {}
export interface DataCollectionProps { export interface DataCollectionProps {
type: typeof DataCollectionType;
collectionConfig: DataCollectionConfig; collectionConfig: DataCollectionConfig;
componentDef: ComponentDefinition; componentDef: ComponentDefinition;
} }

5
packages/core/src/data_sources/model/utils.ts

@ -40,8 +40,7 @@ export function getDataResolverInstance(
resolver = new DataVariable(resolverProps, options); resolver = new DataVariable(resolverProps, options);
break; break;
case DataConditionType: { case DataConditionType: {
const { condition, ifTrue, ifFalse } = resolverProps; resolver = new DataCondition(resolverProps, options);
resolver = new DataCondition(condition, ifTrue, ifFalse, options);
break; break;
} }
case DataCollectionVariableType: { case DataCollectionVariableType: {
@ -64,5 +63,5 @@ export function getDataResolverInstanceValue(
) { ) {
const resolver = getDataResolverInstance(resolverProps, options); const resolver = getDataResolverInstance(resolverProps, options);
return { resolver, value: resolver.getDataValue() }; return resolver.getDataValue();
} }

2
packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts

@ -82,7 +82,7 @@ export class ComponentDataResolverWatchers {
const keys = this.propertyWatcher.getDynamicValuesOfType(DataCollectionVariableType); const keys = this.propertyWatcher.getDynamicValuesOfType(DataCollectionVariableType);
const attributesKeys = this.attributeWatcher.getDynamicValuesOfType(DataCollectionVariableType); const attributesKeys = this.attributeWatcher.getDynamicValuesOfType(DataCollectionVariableType);
const combinedKeys = [keyCollectionsStateMap, ...keys]; const combinedKeys = [keyCollectionsStateMap, 'locked', ...keys];
const haveOverridenAttributes = Object.keys(attributesKeys).length; const haveOverridenAttributes = Object.keys(attributesKeys).length;
if (haveOverridenAttributes) combinedKeys.push('attributes'); if (haveOverridenAttributes) combinedKeys.push('attributes');

11
packages/core/src/dom_components/model/ComponentResolverWatcher.ts

@ -2,7 +2,11 @@ import { ObjectAny } from '../../common';
import { DataCollectionVariableType } from '../../data_sources/model/data_collection/constants'; import { DataCollectionVariableType } from '../../data_sources/model/data_collection/constants';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
import DataResolverListener from '../../data_sources/model/DataResolverListener'; import DataResolverListener from '../../data_sources/model/DataResolverListener';
import { getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/model/utils'; import {
getDataResolverInstance,
getDataResolverInstanceValue,
isDataResolverProps,
} from '../../data_sources/model/utils';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import { DataResolverProps } from '../../data_sources/types'; import { DataResolverProps } from '../../data_sources/types';
import Component from './Component'; import Component from './Component';
@ -90,7 +94,7 @@ export class ComponentResolverWatcher {
continue; continue;
} }
const { resolver } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap }); const resolver = getDataResolverInstance(resolverProps, { em, collectionsStateMap });
this.resolverListeners[key] = new DataResolverListener({ this.resolverListeners[key] = new DataResolverListener({
em, em,
resolver, resolver,
@ -112,8 +116,7 @@ export class ComponentResolverWatcher {
continue; continue;
} }
const { value } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap }); evaluatedValues[key] = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap });
evaluatedValues[key] = value;
} }
return evaluatedValues; return evaluatedValues;

43
packages/core/src/domain_abstract/model/StyleableModel.ts

@ -4,18 +4,19 @@ import ParserHtml from '../../parser/model/ParserHtml';
import Selectors from '../../selector_manager/model/Selectors'; import Selectors from '../../selector_manager/model/Selectors';
import { shallowDiff } from '../../utils/mixins'; import { shallowDiff } from '../../utils/mixins';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import DataVariable, { DataVariableProps, DataVariableType } from '../../data_sources/model/DataVariable'; import { DataVariableProps } from '../../data_sources/model/DataVariable';
import DataResolverListener from '../../data_sources/model/DataResolverListener'; import DataResolverListener from '../../data_sources/model/DataResolverListener';
import CssRuleView from '../../css_composer/view/CssRuleView'; import CssRuleView from '../../css_composer/view/CssRuleView';
import ComponentView from '../../dom_components/view/ComponentView'; import ComponentView from '../../dom_components/view/ComponentView';
import Frame from '../../canvas/model/Frame'; import Frame from '../../canvas/model/Frame';
import { DataConditionProps } from '../../data_sources/model/conditional_variables/DataCondition';
import { import {
DataCondition, getDataResolverInstance,
DataConditionType, getDataResolverInstanceValue,
DataConditionProps, isDataResolver,
} from '../../data_sources/model/conditional_variables/DataCondition'; isDataResolverProps,
import { isDataResolver, isDataResolverProps } from '../../data_sources/model/utils'; } from '../../data_sources/model/utils';
import { DataResolverProps } from '../../data_sources/types'; import { DataResolver } from '../../data_sources/types';
export type StyleProps = Record<string, string | string[] | DataVariableProps | DataConditionProps>; export type StyleProps = Record<string, string | string[] | DataVariableProps | DataConditionProps>;
@ -111,7 +112,8 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
const styleValue = newStyle[key]; const styleValue = newStyle[key];
if (isDataResolverProps(styleValue)) { if (isDataResolverProps(styleValue)) {
const dataResolver = this.getDataResolverInstance(styleValue); const dataResolver = getDataResolverInstance(styleValue, { em: this.em! });
if (dataResolver) { if (dataResolver) {
newStyle[key] = dataResolver; newStyle[key] = dataResolver;
this.listenToDataResolver(dataResolver, key); this.listenToDataResolver(dataResolver, key);
@ -141,25 +143,7 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
return newStyle; return newStyle;
} }
private getDataResolverInstance(props: DataResolverProps) { listenToDataResolver(resolver: DataResolver, styleProp: string) {
const em = this.em!;
let resolver;
switch (props.type) {
case DataVariableType:
resolver = new DataVariable(props, { em });
break;
case DataConditionType: {
const { condition, ifTrue, ifFalse } = props;
resolver = new DataCondition(condition, ifTrue, ifFalse, { em });
break;
}
}
return resolver;
}
listenToDataResolver(resolver: DataVariable | DataCondition, styleProp: string) {
const resolverListener = this.styleResolverListeners[styleProp]; const resolverListener = this.styleResolverListeners[styleProp];
if (resolverListener) { if (resolverListener) {
resolverListener.listenToResolver(); resolverListener.listenToResolver();
@ -203,10 +187,7 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
} }
if (isDataResolverProps(styleValue)) { if (isDataResolverProps(styleValue)) {
const resolver = this.getDataResolverInstance(styleValue); resultStyle[key] = getDataResolverInstanceValue(styleValue, { em: this.em! });
if (resolver) {
resultStyle[key] = resolver.getDataValue();
}
} }
if (isDataResolver(styleValue)) { if (isDataResolver(styleValue)) {

94
packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts

@ -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');
});
});

156
packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts

@ -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);
}

36
packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts → packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts

@ -1,10 +1,7 @@
import { Component, DataSourceManager, Editor } from '../../../../../src'; import { Component, DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import { import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
MissingConditionError, import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator';
DataConditionType,
} from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView'; import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView';
import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
@ -13,7 +10,7 @@ import ComponentTextView from '../../../../../src/dom_components/view/ComponentT
import EditorModel from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor';
import { setupTestEditor } from '../../../../common'; import { setupTestEditor } from '../../../../common';
describe('ComponentConditionalVariable', () => { describe('ComponentDataCondition', () => {
let editor: Editor; let editor: Editor;
let em: EditorModel; let em: EditorModel;
let dsm: DataSourceManager; let dsm: DataSourceManager;
@ -100,7 +97,7 @@ describe('ComponentConditionalVariable', () => {
type: DataVariableType, type: DataVariableType,
path: 'ds1.left_id.left', path: 'ds1.left_id.left',
}, },
operator: GenericOperation.equals, operator: AnyTypeOperation.equals,
right: { right: {
type: DataVariableType, type: DataVariableType,
path: 'ds1.right_id.right', path: 'ds1.right_id.right',
@ -124,10 +121,10 @@ describe('ComponentConditionalVariable', () => {
expect(childComponent.getInnerHTML()).toBe('Some value'); expect(childComponent.getInnerHTML()).toBe('Some value');
/* Test changing datasources */ /* Test changing datasources */
updatedsmLeftValue(dsm, 'Diffirent value'); changeDataSourceValue(dsm, 'Diffirent value');
expect(getFirstChild(component).getInnerHTML()).toBe('False value'); expect(getFirstChild(component).getInnerHTML()).toBe('False value');
expect(getFirstChildView(component)?.el.innerHTML).toBe('False value'); expect(getFirstChildView(component)?.el.innerHTML).toBe('False value');
updatedsmLeftValue(dsm, 'Name1'); changeDataSourceValue(dsm, 'Name1');
expect(getFirstChild(component).getInnerHTML()).toBe('Some value'); expect(getFirstChild(component).getInnerHTML()).toBe('Some value');
expect(getFirstChildView(component)?.el.innerHTML).toBe('Some value'); expect(getFirstChildView(component)?.el.innerHTML).toBe('Some value');
}); });
@ -149,7 +146,7 @@ describe('ComponentConditionalVariable', () => {
type: DataVariableType, type: DataVariableType,
path: 'ds1.left_id.left', path: 'ds1.left_id.left',
}, },
operator: GenericOperation.equals, operator: AnyTypeOperation.equals,
right: { right: {
type: DataVariableType, type: DataVariableType,
path: 'ds1.right_id.right', path: 'ds1.right_id.right',
@ -165,7 +162,7 @@ describe('ComponentConditionalVariable', () => {
type: DataVariableType, type: DataVariableType,
path: 'ds1.left_id.left', path: 'ds1.left_id.left',
}, },
operator: GenericOperation.equals, operator: AnyTypeOperation.equals,
right: { right: {
type: DataVariableType, type: DataVariableType,
path: 'ds1.right_id.right', path: 'ds1.right_id.right',
@ -213,24 +210,9 @@ describe('ComponentConditionalVariable', () => {
const storageCmptDef = frame.component.components[0]; const storageCmptDef = frame.component.components[0];
expect(storageCmptDef).toEqual(conditionalCmptDef); expect(storageCmptDef).toEqual(conditionalCmptDef);
}); });
it('should throw an error if no condition is passed', () => {
const conditionalCmptDef = {
type: DataConditionType,
ifTrue: {
tagName: 'h1',
type: 'text',
content: 'some text',
},
};
expect(() => {
cmpRoot.append(conditionalCmptDef);
}).toThrow(MissingConditionError);
});
}); });
function updatedsmLeftValue(dsm: DataSourceManager, newValue: string) { function changeDataSourceValue(dsm: DataSourceManager, newValue: string) {
dsm.get('ds1').getRecord('left_id')?.set('left', newValue); dsm.get('ds1').getRecord('left_id')?.set('left', newValue);
} }

26
packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts

@ -1,10 +1,7 @@
import { DataSourceManager, Editor } from '../../../../../src'; import { DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import { import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
DataConditionType, import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator';
MissingConditionError,
} from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
import EditorModel from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor';
@ -69,7 +66,7 @@ describe('StyleConditionalVariable', () => {
type: DataVariableType, type: DataVariableType,
path: 'ds1.left_id.left', path: 'ds1.left_id.left',
}, },
operator: GenericOperation.equals, operator: AnyTypeOperation.equals,
right: { right: {
type: DataVariableType, type: DataVariableType,
path: 'ds1.right_id.right', path: 'ds1.right_id.right',
@ -87,23 +84,6 @@ describe('StyleConditionalVariable', () => {
expect(component.getStyle().color).toBe('green'); expect(component.getStyle().color).toBe('green');
}); });
it('should throw an error if no condition is passed in style', () => {
expect(() => {
cmpRoot.append({
tagName: 'h1',
type: 'text',
content: 'some text',
style: {
color: {
type: DataConditionType,
ifTrue: 'grey',
ifFalse: 'red',
},
},
});
}).toThrow(MissingConditionError);
});
it.skip('should store components with conditional styles correctly', () => { it.skip('should store components with conditional styles correctly', () => {
const conditionalStyleDef = { const conditionalStyleDef = {
tagName: 'h1', tagName: 'h1',

170
packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts

@ -4,10 +4,10 @@ import {
ExpressionProps, ExpressionProps,
LogicGroupProps, LogicGroupProps,
} from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator';
import { LogicalOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator'; import { BooleanOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/BooleanOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperator';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import Editor from '../../../../../src/editor/model/Editor'; import Editor from '../../../../../src/editor/model/Editor';
import EditorModel from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor';
@ -36,23 +36,41 @@ describe('DataCondition', () => {
describe('Basic Functionality Tests', () => { describe('Basic Functionality Tests', () => {
test('should evaluate a simple boolean condition', () => { test('should evaluate a simple boolean condition', () => {
const condition = true; const condition = true;
const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); const dataCondition = new DataCondition({ condition, ifTrue: 'Yes', ifFalse: 'No' }, { em });
expect(dataCondition.getDataValue()).toBe('Yes'); expect(dataCondition.getDataValue()).toBe('Yes');
}); });
test('should return ifFalse when condition evaluates to false', () => { test('should return ifFalse when condition evaluates to false', () => {
const condition = false; const condition = false;
const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); const dataCondition = new DataCondition({ condition, ifTrue: 'Yes', ifFalse: 'No' }, { em });
expect(dataCondition.getDataValue()).toBe('No'); expect(dataCondition.getDataValue()).toBe('No');
}); });
test('should return raw ifTrue value when skipDynamicValueResolution is true and condition is true', () => {
const condition = true;
const ifTrue = { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' };
const ifFalse = 'No';
const dataCondition = new DataCondition({ condition, ifTrue, ifFalse }, { em });
expect(dataCondition.getDataValue(true)).toEqual(ifTrue);
});
test('should return raw ifFalse value when skipDynamicValueResolution is true and condition is false', () => {
const condition = false;
const ifTrue = 'Yes';
const ifFalse = { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' };
const dataCondition = new DataCondition({ condition, ifTrue, ifFalse }, { em });
expect(dataCondition.getDataValue(true)).toEqual(ifFalse);
});
}); });
describe('Operator Tests', () => { describe('Operator Tests', () => {
test('should evaluate using GenericOperation operators', () => { test('should evaluate using GenericOperation operators', () => {
const condition: ExpressionProps = { left: 5, operator: GenericOperation.equals, right: 5 }; const condition: ExpressionProps = { left: 5, operator: AnyTypeOperation.equals, right: 5 };
const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal', { em }); const dataCondition = new DataCondition({ condition, ifTrue: 'Equal', ifFalse: 'Not Equal' }, { em });
expect(dataCondition.getDataValue()).toBe('Equal'); expect(dataCondition.getDataValue()).toBe('Equal');
}); });
@ -60,63 +78,61 @@ describe('DataCondition', () => {
test('equals (false)', () => { test('equals (false)', () => {
const condition: ExpressionProps = { const condition: ExpressionProps = {
left: 'hello', left: 'hello',
operator: GenericOperation.equals, operator: AnyTypeOperation.equals,
right: 'world', right: 'world',
}; };
const dataCondition = new DataCondition(condition, 'true', 'false', { em }); const dataCondition = new DataCondition({ condition, ifTrue: 'true', ifFalse: 'false' }, { em });
expect(dataCondition.evaluate()).toBe(false); expect(dataCondition.isTrue()).toBe(false);
}); });
test('should evaluate using StringOperation operators', () => { test('should evaluate using StringOperation operators', () => {
const condition: ExpressionProps = { left: 'apple', operator: StringOperation.contains, right: 'app' }; const condition: ExpressionProps = { left: 'apple', operator: StringOperation.contains, right: 'app' };
const dataCondition = new DataCondition(condition, 'Contains', "Doesn't contain", { em }); const dataCondition = new DataCondition({ condition, ifTrue: 'Contains', ifFalse: "Doesn't contain" }, { em });
expect(dataCondition.getDataValue()).toBe('Contains'); expect(dataCondition.getDataValue()).toBe('Contains');
}); });
test('should evaluate using NumberOperation operators', () => { test('should evaluate using NumberOperation operators', () => {
const condition: ExpressionProps = { left: 10, operator: NumberOperation.lessThan, right: 15 }; const condition: ExpressionProps = { left: 10, operator: NumberOperation.lessThan, right: 15 };
const dataCondition = new DataCondition(condition, 'Valid', 'Invalid', { em }); const dataCondition = new DataCondition({ condition, ifTrue: 'Valid', ifFalse: 'Invalid' }, { em });
expect(dataCondition.getDataValue()).toBe('Valid'); expect(dataCondition.getDataValue()).toBe('Valid');
}); });
test('should evaluate using LogicalOperation operators', () => { test('should evaluate using LogicalOperation operators', () => {
const logicGroup: LogicGroupProps = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and, logicalOperator: BooleanOperation.and,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: true }, { left: true, operator: AnyTypeOperation.equals, right: true },
{ left: 5, operator: NumberOperation.greaterThan, right: 3 }, { left: 5, operator: NumberOperation.greaterThan, right: 3 },
], ],
}; };
const dataCondition = new DataCondition(logicGroup, 'Pass', 'Fail', { em }); const dataCondition = new DataCondition({ condition: logicGroup, ifTrue: 'Pass', ifFalse: 'Fail' }, { em });
expect(dataCondition.getDataValue()).toBe('Pass'); expect(dataCondition.getDataValue()).toBe('Pass');
}); });
}); });
describe('Edge Case Tests', () => { describe('Edge Case Tests', () => {
test('should throw error for invalid condition type', () => {
const invalidCondition: any = { randomField: 'randomValue' };
expect(() => new DataCondition(invalidCondition, 'Yes', 'No', { em })).toThrow('Invalid condition type.');
});
test('should evaluate complex nested conditions', () => { test('should evaluate complex nested conditions', () => {
const nestedLogicGroup: LogicGroupProps = { const nestedLogicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.or, logicalOperator: BooleanOperation.or,
statements: [ statements: [
{ {
logicalOperator: LogicalOperation.and, logicalOperator: BooleanOperation.and,
statements: [ statements: [
{ left: 1, operator: NumberOperation.lessThan, right: 5 }, { left: 1, operator: NumberOperation.lessThan, right: 5 },
{ left: 'test', operator: GenericOperation.equals, right: 'test' }, { left: 'test', operator: AnyTypeOperation.equals, right: 'test' },
], ],
}, },
{ left: 10, operator: NumberOperation.greaterThan, right: 100 }, { left: 10, operator: NumberOperation.greaterThan, right: 100 },
], ],
}; };
const dataCondition = new DataCondition(nestedLogicGroup, 'Nested Pass', 'Nested Fail', { em }); const dataCondition = new DataCondition(
{ condition: nestedLogicGroup, ifTrue: 'Nested Pass', ifFalse: 'Nested Fail' },
{ em },
);
expect(dataCondition.getDataValue()).toBe('Nested Pass'); expect(dataCondition.getDataValue()).toBe('Nested Pass');
}); });
}); });
@ -124,74 +140,89 @@ describe('DataCondition', () => {
describe('LogicalGroup Tests', () => { describe('LogicalGroup Tests', () => {
test('should correctly handle AND logical operator', () => { test('should correctly handle AND logical operator', () => {
const logicGroup: LogicGroupProps = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and, logicalOperator: BooleanOperation.and,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: true }, { left: true, operator: AnyTypeOperation.equals, right: true },
{ left: 5, operator: NumberOperation.greaterThan, right: 3 }, { left: 5, operator: NumberOperation.greaterThan, right: 3 },
], ],
}; };
const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false', { em }); const dataCondition = new DataCondition(
{ condition: logicGroup, ifTrue: 'All true', ifFalse: 'One or more false' },
{ em },
);
expect(dataCondition.getDataValue()).toBe('All true'); expect(dataCondition.getDataValue()).toBe('All true');
}); });
test('should correctly handle OR logical operator', () => { test('should correctly handle OR logical operator', () => {
const logicGroup: LogicGroupProps = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.or, logicalOperator: BooleanOperation.or,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: false }, { left: true, operator: AnyTypeOperation.equals, right: false },
{ left: 5, operator: NumberOperation.greaterThan, right: 3 }, { left: 5, operator: NumberOperation.greaterThan, right: 3 },
], ],
}; };
const dataCondition = new DataCondition(logicGroup, 'At least one true', 'All false', { em }); const dataCondition = new DataCondition(
{ condition: logicGroup, ifTrue: 'At least one true', ifFalse: 'All false' },
{ em },
);
expect(dataCondition.getDataValue()).toBe('At least one true'); expect(dataCondition.getDataValue()).toBe('At least one true');
}); });
test('should correctly handle XOR logical operator', () => { test('should correctly handle XOR logical operator', () => {
const logicGroup: LogicGroupProps = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.xor, logicalOperator: BooleanOperation.xor,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: true }, { left: true, operator: AnyTypeOperation.equals, right: true },
{ left: 5, operator: NumberOperation.lessThan, right: 3 }, { left: 5, operator: NumberOperation.lessThan, right: 3 },
{ left: false, operator: GenericOperation.equals, right: true }, { left: false, operator: AnyTypeOperation.equals, right: true },
], ],
}; };
const dataCondition = new DataCondition(logicGroup, 'Exactly one true', 'Multiple true or all false', { em }); const dataCondition = new DataCondition(
{ condition: logicGroup, ifTrue: 'Exactly one true', ifFalse: 'Multiple true or all false' },
{ em },
);
expect(dataCondition.getDataValue()).toBe('Exactly one true'); expect(dataCondition.getDataValue()).toBe('Exactly one true');
}); });
test('should handle nested logical groups', () => { test('should handle nested logical groups', () => {
const logicGroup: LogicGroupProps = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and, logicalOperator: BooleanOperation.and,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: true }, { left: true, operator: AnyTypeOperation.equals, right: true },
{ {
logicalOperator: LogicalOperation.or, logicalOperator: BooleanOperation.or,
statements: [ statements: [
{ left: 5, operator: NumberOperation.greaterThan, right: 3 }, { left: 5, operator: NumberOperation.greaterThan, right: 3 },
{ left: false, operator: GenericOperation.equals, right: true }, { left: false, operator: AnyTypeOperation.equals, right: true },
], ],
}, },
], ],
}; };
const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false', { em }); const dataCondition = new DataCondition(
{ condition: logicGroup, ifTrue: 'All true', ifFalse: 'One or more false' },
{ em },
);
expect(dataCondition.getDataValue()).toBe('All true'); expect(dataCondition.getDataValue()).toBe('All true');
}); });
test('should handle groups with false conditions', () => { test('should handle groups with false conditions', () => {
const logicGroup: LogicGroupProps = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and, logicalOperator: BooleanOperation.and,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: true }, { left: true, operator: AnyTypeOperation.equals, right: true },
{ left: false, operator: GenericOperation.equals, right: true }, { left: false, operator: AnyTypeOperation.equals, right: true },
{ left: 5, operator: NumberOperation.greaterThan, right: 3 }, { left: 5, operator: NumberOperation.greaterThan, right: 3 },
], ],
}; };
const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false', { em }); const dataCondition = new DataCondition(
{ condition: logicGroup, ifTrue: 'All true', ifFalse: 'One or more false' },
{ em },
);
expect(dataCondition.getDataValue()).toBe('One or more false'); expect(dataCondition.getDataValue()).toBe('One or more false');
}); });
}); });
@ -200,22 +231,22 @@ describe('DataCondition', () => {
test('should return "Yes" when dataVariable matches expected value', () => { test('should return "Yes" when dataVariable matches expected value', () => {
const condition: ExpressionProps = { const condition: ExpressionProps = {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
operator: GenericOperation.equals, operator: AnyTypeOperation.equals,
right: 'active', right: 'active',
}; };
const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); const dataCondition = new DataCondition({ condition, ifTrue: 'Yes', ifFalse: 'No' }, { em });
expect(dataCondition.getDataValue()).toBe('Yes'); expect(dataCondition.getDataValue()).toBe('Yes');
}); });
test('should return "No" when dataVariable does not match expected value', () => { test('should return "No" when dataVariable does not match expected value', () => {
const condition: ExpressionProps = { const condition: ExpressionProps = {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
operator: GenericOperation.equals, operator: AnyTypeOperation.equals,
right: 'inactive', right: 'inactive',
}; };
const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); const dataCondition = new DataCondition({ condition, ifTrue: 'Yes', ifFalse: 'No' }, { em });
expect(dataCondition.getDataValue()).toBe('No'); expect(dataCondition.getDataValue()).toBe('No');
}); });
@ -223,11 +254,11 @@ describe('DataCondition', () => {
test.skip('should handle missing data variable gracefully', () => { test.skip('should handle missing data variable gracefully', () => {
const condition: ExpressionProps = { const condition: ExpressionProps = {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.not_a_user.status' }, left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.not_a_user.status' },
operator: GenericOperation.isDefined, operator: AnyTypeOperation.isDefined,
right: undefined, right: undefined,
}; };
const dataCondition = new DataCondition(condition, 'Found', 'Not Found', { em }); const dataCondition = new DataCondition({ condition, ifTrue: 'Found', ifFalse: 'Not Found' }, { em });
expect(dataCondition.getDataValue()).toBe('Not Found'); expect(dataCondition.getDataValue()).toBe('Not Found');
}); });
@ -237,7 +268,7 @@ describe('DataCondition', () => {
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,
right: 24, right: 24,
}; };
const dataCondition = new DataCondition(condition, 'Valid', 'Invalid', { em }); const dataCondition = new DataCondition({ condition, ifTrue: 'Valid', ifFalse: 'Invalid' }, { em });
expect(dataCondition.getDataValue()).toBe('Valid'); expect(dataCondition.getDataValue()).toBe('Valid');
}); });
@ -249,11 +280,11 @@ describe('DataCondition', () => {
dsm.add(dataSource2); dsm.add(dataSource2);
const logicGroup: LogicGroupProps = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and, logicalOperator: BooleanOperation.and,
statements: [ statements: [
{ {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
operator: GenericOperation.equals, operator: AnyTypeOperation.equals,
right: 'active', right: 'active',
}, },
{ {
@ -264,20 +295,23 @@ describe('DataCondition', () => {
], ],
}; };
const dataCondition = new DataCondition(logicGroup, 'All conditions met', 'Some conditions failed', { em }); const dataCondition = new DataCondition(
{ condition: logicGroup, ifTrue: 'All conditions met', ifFalse: 'Some conditions failed' },
{ em },
);
expect(dataCondition.getDataValue()).toBe('All conditions met'); expect(dataCondition.getDataValue()).toBe('All conditions met');
}); });
test('should handle nested logical conditions with data variables', () => { test('should handle nested logical conditions with data variables', () => {
const logicGroup: LogicGroupProps = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.or, logicalOperator: BooleanOperation.or,
statements: [ statements: [
{ {
logicalOperator: LogicalOperation.and, logicalOperator: BooleanOperation.and,
statements: [ statements: [
{ {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_2.status' }, left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_2.status' },
operator: GenericOperation.equals, operator: AnyTypeOperation.equals,
right: 'inactive', right: 'inactive',
}, },
{ {
@ -289,21 +323,26 @@ describe('DataCondition', () => {
}, },
{ {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
operator: GenericOperation.equals, operator: AnyTypeOperation.equals,
right: 'inactive', right: 'inactive',
}, },
], ],
}; };
const dataCondition = new DataCondition(logicGroup, 'Condition met', 'Condition failed', { em }); const dataCondition = new DataCondition(
{ condition: logicGroup, ifTrue: 'Condition met', ifFalse: 'Condition failed' },
{ em },
);
expect(dataCondition.getDataValue()).toBe('Condition met'); expect(dataCondition.getDataValue()).toBe('Condition met');
}); });
test('should handle data variables as an ifTrue return value', () => { test('should handle data variables as an ifTrue return value', () => {
const dataCondition = new DataCondition( const dataCondition = new DataCondition(
true, {
{ type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, condition: true,
'No', ifTrue: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
ifFalse: 'No',
},
{ em }, { em },
); );
expect(dataCondition.getDataValue()).toBe('active'); expect(dataCondition.getDataValue()).toBe('active');
@ -311,11 +350,14 @@ describe('DataCondition', () => {
test('should handle data variables as an ifFalse return value', () => { test('should handle data variables as an ifFalse return value', () => {
const dataCondition = new DataCondition( const dataCondition = new DataCondition(
false, {
'Yes', condition: false,
{ type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, ifTrue: 'Yes',
ifFalse: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
},
{ em }, { em },
); );
expect(dataCondition.getDataValue()).toBe('active'); expect(dataCondition.getDataValue()).toBe('active');
}); });
}); });

69
packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts → packages/core/test/specs/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts

@ -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');
});
}); });
}); });

39
packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts → packages/core/test/specs/data_sources/model/conditional_variables/operators/BooleanOperator.ts

@ -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');
});
}); });
}); });

45
packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts

@ -2,94 +2,101 @@ import {
NumberOperator, NumberOperator,
NumberOperation, NumberOperation,
} from '../../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; } from '../../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import Editor from '../../../../../../src/editor/model/Editor';
import EditorModel from '../../../../../../src/editor/model/Editor';
describe('NumberOperator', () => { describe('NumberOperator', () => {
let em: EditorModel;
beforeEach(() => {
em = new Editor();
});
afterEach(() => {
em.destroy();
});
describe('Operator: greaterThan', () => { describe('Operator: greaterThan', () => {
test('should return true when left is greater than right', () => { test('should return true when left is greater than right', () => {
const operator = new NumberOperator(NumberOperation.greaterThan); const operator = new NumberOperator(NumberOperation.greaterThan, { em });
expect(operator.evaluate(5, 3)).toBe(true); expect(operator.evaluate(5, 3)).toBe(true);
}); });
test('should return false when left is not greater than right', () => { test('should return false when left is not greater than right', () => {
const operator = new NumberOperator(NumberOperation.greaterThan); const operator = new NumberOperator(NumberOperation.greaterThan, { em });
expect(operator.evaluate(2, 3)).toBe(false); expect(operator.evaluate(2, 3)).toBe(false);
}); });
}); });
describe('Operator: lessThan', () => { describe('Operator: lessThan', () => {
test('should return true when left is less than right', () => { test('should return true when left is less than right', () => {
const operator = new NumberOperator(NumberOperation.lessThan); const operator = new NumberOperator(NumberOperation.lessThan, { em });
expect(operator.evaluate(2, 3)).toBe(true); expect(operator.evaluate(2, 3)).toBe(true);
}); });
test('should return false when left is not less than right', () => { test('should return false when left is not less than right', () => {
const operator = new NumberOperator(NumberOperation.lessThan); const operator = new NumberOperator(NumberOperation.lessThan, { em });
expect(operator.evaluate(5, 3)).toBe(false); expect(operator.evaluate(5, 3)).toBe(false);
}); });
}); });
describe('Operator: greaterThanOrEqual', () => { describe('Operator: greaterThanOrEqual', () => {
test('should return true when left is greater than or equal to right', () => { test('should return true when left is greater than or equal to right', () => {
const operator = new NumberOperator(NumberOperation.greaterThanOrEqual); const operator = new NumberOperator(NumberOperation.greaterThanOrEqual, { em });
expect(operator.evaluate(3, 3)).toBe(true); expect(operator.evaluate(3, 3)).toBe(true);
}); });
test('should return false when left is not greater than or equal to right', () => { test('should return false when left is not greater than or equal to right', () => {
const operator = new NumberOperator(NumberOperation.greaterThanOrEqual); const operator = new NumberOperator(NumberOperation.greaterThanOrEqual, { em });
expect(operator.evaluate(2, 3)).toBe(false); expect(operator.evaluate(2, 3)).toBe(false);
}); });
}); });
describe('Operator: lessThanOrEqual', () => { describe('Operator: lessThanOrEqual', () => {
test('should return true when left is less than or equal to right', () => { test('should return true when left is less than or equal to right', () => {
const operator = new NumberOperator(NumberOperation.lessThanOrEqual); const operator = new NumberOperator(NumberOperation.lessThanOrEqual, { em });
expect(operator.evaluate(3, 3)).toBe(true); expect(operator.evaluate(3, 3)).toBe(true);
}); });
test('should return false when left is not less than or equal to right', () => { test('should return false when left is not less than or equal to right', () => {
const operator = new NumberOperator(NumberOperation.lessThanOrEqual); const operator = new NumberOperator(NumberOperation.lessThanOrEqual, { em });
expect(operator.evaluate(5, 3)).toBe(false); expect(operator.evaluate(5, 3)).toBe(false);
}); });
}); });
describe('Operator: equals', () => { describe('Operator: equals', () => {
test('should return true when numbers are equal', () => { test('should return true when numbers are equal', () => {
const operator = new NumberOperator(NumberOperation.equals); const operator = new NumberOperator(NumberOperation.equals, { em });
expect(operator.evaluate(4, 4)).toBe(true); expect(operator.evaluate(4, 4)).toBe(true);
}); });
test('should return false when numbers are not equal', () => { test('should return false when numbers are not equal', () => {
const operator = new NumberOperator(NumberOperation.equals); const operator = new NumberOperator(NumberOperation.equals, { em });
expect(operator.evaluate(4, 5)).toBe(false); expect(operator.evaluate(4, 5)).toBe(false);
}); });
}); });
describe('Operator: notEquals', () => { describe('Operator: notEquals', () => {
test('should return true when numbers are not equal', () => { test('should return true when numbers are not equal', () => {
const operator = new NumberOperator(NumberOperation.notEquals); const operator = new NumberOperator(NumberOperation.notEquals, { em });
expect(operator.evaluate(4, 5)).toBe(true); expect(operator.evaluate(4, 5)).toBe(true);
}); });
test('should return false when numbers are equal', () => { test('should return false when numbers are equal', () => {
const operator = new NumberOperator(NumberOperation.notEquals); const operator = new NumberOperator(NumberOperation.notEquals, { em });
expect(operator.evaluate(4, 4)).toBe(false); expect(operator.evaluate(4, 4)).toBe(false);
}); });
}); });
describe('Edge Case Tests', () => { describe('Edge Case Tests', () => {
test('should handle boundary values correctly', () => { test('should handle boundary values correctly', () => {
const operator = new NumberOperator(NumberOperation.lessThan); const operator = new NumberOperator(NumberOperation.lessThan, { em });
expect(operator.evaluate(Number.MIN_VALUE, 1)).toBe(true); expect(operator.evaluate(Number.MIN_VALUE, 1)).toBe(true);
}); });
test('should return false for NaN comparisons', () => { test('should return false for NaN comparisons', () => {
const operator = new NumberOperator(NumberOperation.equals); const operator = new NumberOperator(NumberOperation.equals, { em });
expect(operator.evaluate(NaN, NaN)).toBe(false); expect(operator.evaluate(NaN, NaN)).toBe(false);
}); });
test('should throw error for unsupported operator', () => {
const operator = new NumberOperator('unsupported' as NumberOperation);
expect(() => operator.evaluate(1, 2)).toThrow('Unsupported number operator: unsupported');
});
}); });
}); });

55
packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts

@ -1,70 +1,82 @@
import { import {
StringOperator, StringOperator,
StringOperation, StringOperation,
} from '../../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; } from '../../../../../../src/data_sources/model/conditional_variables/operators/StringOperator';
import Editor from '../../../../../../src/editor/model/Editor';
import EditorModel from '../../../../../../src/editor/model/Editor';
describe('StringOperator', () => { describe('StringOperator', () => {
let em: EditorModel;
beforeEach(() => {
em = new Editor();
});
afterEach(() => {
em.destroy();
});
describe('Operator: contains', () => { describe('Operator: contains', () => {
test('should return true when left contains right', () => { test('should return true when left contains right', () => {
const operator = new StringOperator(StringOperation.contains); const operator = new StringOperator(StringOperation.contains, { em });
expect(operator.evaluate('hello world', 'world')).toBe(true); expect(operator.evaluate('hello world', 'world')).toBe(true);
}); });
test('should return false when left does not contain right', () => { test('should return false when left does not contain right', () => {
const operator = new StringOperator(StringOperation.contains); const operator = new StringOperator(StringOperation.contains, { em });
expect(operator.evaluate('hello world', 'moon')).toBe(false); expect(operator.evaluate('hello world', 'moon')).toBe(false);
}); });
}); });
describe('Operator: startsWith', () => { describe('Operator: startsWith', () => {
test('should return true when left starts with right', () => { test('should return true when left starts with right', () => {
const operator = new StringOperator(StringOperation.startsWith); const operator = new StringOperator(StringOperation.startsWith, { em });
expect(operator.evaluate('hello world', 'hello')).toBe(true); expect(operator.evaluate('hello world', 'hello')).toBe(true);
}); });
test('should return false when left does not start with right', () => { test('should return false when left does not start with right', () => {
const operator = new StringOperator(StringOperation.startsWith); const operator = new StringOperator(StringOperation.startsWith, { em });
expect(operator.evaluate('hello world', 'world')).toBe(false); expect(operator.evaluate('hello world', 'world')).toBe(false);
}); });
}); });
describe('Operator: endsWith', () => { describe('Operator: endsWith', () => {
test('should return true when left ends with right', () => { test('should return true when left ends with right', () => {
const operator = new StringOperator(StringOperation.endsWith); const operator = new StringOperator(StringOperation.endsWith, { em });
expect(operator.evaluate('hello world', 'world')).toBe(true); expect(operator.evaluate('hello world', 'world')).toBe(true);
}); });
test('should return false when left does not end with right', () => { test('should return false when left does not end with right', () => {
const operator = new StringOperator(StringOperation.endsWith); const operator = new StringOperator(StringOperation.endsWith, { em });
expect(operator.evaluate('hello world', 'hello')).toBe(false); expect(operator.evaluate('hello world', 'hello')).toBe(false);
}); });
}); });
describe('Operator: matchesRegex', () => { describe('Operator: matchesRegex', () => {
test('should return true when left matches the regex right', () => { test('should return true when left matches the regex right', () => {
const operator = new StringOperator(StringOperation.matchesRegex); const operator = new StringOperator(StringOperation.matchesRegex, { em });
expect(operator.evaluate('hello world', '^hello')).toBe(true); expect(operator.evaluate('hello world', '^hello')).toBe(true);
}); });
test('should return false when left does not match the regex right', () => { test('should return false when left does not match the regex right', () => {
const operator = new StringOperator(StringOperation.matchesRegex); const operator = new StringOperator(StringOperation.matchesRegex, { em });
expect(operator.evaluate('hello world', '^world')).toBe(false); expect(operator.evaluate('hello world', '^world')).toBe(false);
}); });
}); });
describe('Operator: equalsIgnoreCase', () => { describe('Operator: equalsIgnoreCase', () => {
test('should return true when left equals right ignoring case', () => { test('should return true when left equals right ignoring case', () => {
const operator = new StringOperator(StringOperation.equalsIgnoreCase); const operator = new StringOperator(StringOperation.equalsIgnoreCase, { em });
expect(operator.evaluate('Hello World', 'hello world')).toBe(true); expect(operator.evaluate('Hello World', 'hello world')).toBe(true);
}); });
test('should return false when left does not equal right ignoring case', () => { test('should return false when left does not equal right ignoring case', () => {
const operator = new StringOperator(StringOperation.equalsIgnoreCase); const operator = new StringOperator(StringOperation.equalsIgnoreCase, { em });
expect(operator.evaluate('Hello World', 'hello there')).toBe(false); expect(operator.evaluate('Hello World', 'hello there')).toBe(false);
}); });
test('should handle empty strings correctly', () => { test('should handle empty strings correctly', () => {
const operator = new StringOperator(StringOperation.equalsIgnoreCase); const operator = new StringOperator(StringOperation.equalsIgnoreCase, { em });
expect(operator.evaluate('', '')).toBe(true); expect(operator.evaluate('', '')).toBe(true);
expect(operator.evaluate('Hello', '')).toBe(false); expect(operator.evaluate('Hello', '')).toBe(false);
expect(operator.evaluate('', 'Hello')).toBe(false); expect(operator.evaluate('', 'Hello')).toBe(false);
@ -73,17 +85,17 @@ describe('StringOperator', () => {
describe('Operator: trimEquals', () => { describe('Operator: trimEquals', () => {
test('should return true when left equals right after trimming', () => { test('should return true when left equals right after trimming', () => {
const operator = new StringOperator(StringOperation.trimEquals); const operator = new StringOperator(StringOperation.trimEquals, { em });
expect(operator.evaluate(' Hello World ', 'Hello World')).toBe(true); expect(operator.evaluate(' Hello World ', 'Hello World')).toBe(true);
}); });
test('should return false when left does not equal right after trimming', () => { test('should return false when left does not equal right after trimming', () => {
const operator = new StringOperator(StringOperation.trimEquals); const operator = new StringOperator(StringOperation.trimEquals, { em });
expect(operator.evaluate(' Hello World ', 'Hello there')).toBe(false); expect(operator.evaluate(' Hello World ', 'Hello there')).toBe(false);
}); });
test('should handle cases with only whitespace', () => { test('should handle cases with only whitespace', () => {
const operator = new StringOperator(StringOperation.trimEquals); const operator = new StringOperator(StringOperation.trimEquals, { em });
expect(operator.evaluate(' ', '')).toBe(true); // Both should trim to empty expect(operator.evaluate(' ', '')).toBe(true); // Both should trim to empty
expect(operator.evaluate(' ', 'non-empty')).toBe(false); expect(operator.evaluate(' ', 'non-empty')).toBe(false);
}); });
@ -91,28 +103,23 @@ describe('StringOperator', () => {
describe('Edge Case Tests', () => { describe('Edge Case Tests', () => {
test('should return false for contains with empty right string', () => { test('should return false for contains with empty right string', () => {
const operator = new StringOperator(StringOperation.contains); const operator = new StringOperator(StringOperation.contains, { em });
expect(operator.evaluate('hello world', '')).toBe(true); // Empty string is included in any string expect(operator.evaluate('hello world', '')).toBe(true); // Empty string is included in any string
}); });
test('should return true for startsWith with empty right string', () => { test('should return true for startsWith with empty right string', () => {
const operator = new StringOperator(StringOperation.startsWith); const operator = new StringOperator(StringOperation.startsWith, { em });
expect(operator.evaluate('hello world', '')).toBe(true); // Any string starts with an empty string expect(operator.evaluate('hello world', '')).toBe(true); // Any string starts with an empty string
}); });
test('should return true for endsWith with empty right string', () => { test('should return true for endsWith with empty right string', () => {
const operator = new StringOperator(StringOperation.endsWith); const operator = new StringOperator(StringOperation.endsWith, { em });
expect(operator.evaluate('hello world', '')).toBe(true); // Any string ends with an empty string expect(operator.evaluate('hello world', '')).toBe(true); // Any string ends with an empty string
}); });
test('should throw error for invalid regex', () => { test('should throw error for invalid regex', () => {
const operator = new StringOperator(StringOperation.matchesRegex); const operator = new StringOperator(StringOperation.matchesRegex, { em });
expect(() => operator.evaluate('hello world', '[')).toThrow(); expect(() => operator.evaluate('hello world', '[')).toThrow();
}); });
test('should throw error for unsupported operator', () => {
const operator = new StringOperator('unsupported' as StringOperation);
expect(() => operator.evaluate('test', 'test')).toThrow('Unsupported string operator: unsupported');
});
}); });
}); });

350
packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts

@ -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…
Cancel
Save