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,
path: '',
defaultValue: '',
droppable: false,
};
}
@ -22,6 +23,14 @@ export default class ComponentDataVariable extends Component {
this.dataResolver = new DataVariable({ type, path, defaultValue }, opt);
}
getPath() {
return this.dataResolver.get('path');
}
getDefaultValue() {
return this.dataResolver.get('defaultValue');
}
getDataValue() {
return this.dataResolver.getDataValue();
}
@ -30,6 +39,14 @@ export default class ComponentDataVariable extends Component {
return this.getDataValue();
}
setPath(newPath: string) {
this.dataResolver.set('path', newPath);
}
setDefaultValue(newValue: string) {
this.dataResolver.set('defaultValue', newValue);
}
static isComponent(el: HTMLElement) {
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;
}
interface ListenerWithCallback extends DataSourceListener {
callback: () => void;
}
export default class DataResolverListener {
private listeners: DataSourceListener[] = [];
private listeners: ListenerWithCallback[] = [];
private em: EditorModel;
private onUpdate: (value: any) => void;
private model = new Model();
@ -33,10 +37,14 @@ export default class DataResolverListener {
this.onUpdate(value);
};
private createListener(obj: any, event: string, callback: () => void = this.onChange): ListenerWithCallback {
return { obj, event, callback };
}
listenToResolver() {
const { resolver, model } = this;
this.removeListeners();
let listeners: DataSourceListener[] = [];
let listeners: ListenerWithCallback[] = [];
const type = resolver.attributes.type;
switch (type) {
@ -51,11 +59,11 @@ export default class DataResolverListener {
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;
}
private listenToConditionalVariable(dataVariable: DataCondition) {
private listenToConditionalVariable(dataVariable: DataCondition): ListenerWithCallback[] {
const { em } = this;
const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => {
return this.listenToDataVariable(new DataVariable(dataVariable, { em }));
@ -64,29 +72,41 @@ export default class DataResolverListener {
return dataListeners;
}
private listenToDataVariable(dataVariable: DataVariable) {
private listenToDataVariable(dataVariable: DataVariable): ListenerWithCallback[] {
const { em } = this;
const dataListeners: DataSourceListener[] = [];
const { path } = dataVariable.attributes;
const normPath = stringToPath(path || '').join('.');
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(
{ obj: dataVariable, event: 'change:path change:defaultValue' },
{ obj: em.DataSources.all, event: 'add remove reset' },
{ obj: em, event: `${DataSourcesEvents.path}:${normPath}` },
this.createListener(dataVariable, 'change:path', () => {
this.listenToResolver();
this.onChange();
}),
this.createListener(dataVariable, 'change:defaultValue'),
this.createListener(em.DataSources.all, 'add remove reset'),
this.createListener(em, `${DataSourcesEvents.path}:${normPath}`),
);
return dataListeners;
}
private listenToDataCollectionVariable(dataVariable: DataCollectionVariable) {
return [{ obj: dataVariable, event: 'change:value' }];
private listenToDataCollectionVariable(dataVariable: DataCollectionVariable): ListenerWithCallback[] {
return [this.createListener(dataVariable, 'change:value')];
}
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 = [];
}

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 { toLowerCase } from '../../../utils/mixins';
import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition';
import { ConditionProps } from './DataConditionEvaluator';
export default class ComponentDataCondition extends Component {
dataResolver: DataCondition;
constructor(props: DataConditionProps, opt: ComponentOptions) {
const { condition, ifTrue, ifFalse } = props;
const dataConditionInstance = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em });
const dataConditionInstance = new DataCondition(props, { em: opt.em });
super(
{
...props,
type: DataConditionType,
components: dataConditionInstance.getDataValue(),
droppable: false,
},
opt,
);
@ -21,8 +23,19 @@ export default class ComponentDataCondition extends Component {
this.dataResolver.onValueChange = this.handleConditionChange.bind(this);
}
getCondition() {
return this.dataResolver.getCondition();
}
getIfTrue() {
return this.dataResolver.getIfTrue();
}
getIfFalse() {
return this.dataResolver.getIfFalse();
}
private handleConditionChange() {
this.dataResolver.reevaluate();
this.components(this.dataResolver.getDataValue());
}
@ -30,6 +43,18 @@ export default class ComponentDataCondition extends Component {
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 {
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 {
constructor(
private leftValue: any,
private operator: Operator,
private operator: Operator<DataConditionOperation>,
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 DataResolverListener from '../DataResolverListener';
import { evaluateVariable, isDataVariable } from '../utils';
import { Condition, ConditionProps } from './Condition';
import { GenericOperation } from './operators/GenericOperator';
import { LogicalOperation } from './operators/LogicalOperator';
import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator';
import { AnyTypeOperation } from './operators/AnyTypeOperator';
import { BooleanOperation } from './operators/BooleanOperator';
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 interface ExpressionProps {
left: any;
operator: GenericOperation | StringOperation | NumberOperation;
operator: AnyTypeOperation | StringOperation | NumberOperation;
right: any;
}
export interface LogicGroupProps {
logicalOperator: LogicalOperation;
logicalOperator: BooleanOperation;
statements: ConditionProps[];
}
@ -30,57 +31,90 @@ export interface DataConditionProps {
}
interface DataConditionPropsDefined extends Omit<DataConditionProps, 'condition'> {
condition: Condition;
condition: DataConditionEvaluator;
}
export class DataCondition extends Model<DataConditionPropsDefined> {
lastEvaluationResult: boolean;
private em: EditorModel;
private resolverListeners: DataResolverListener[] = [];
private _onValueChange?: () => void;
constructor(
condition: ConditionProps,
public ifTrue: any,
public ifFalse: any,
props: {
condition: ConditionProps;
ifTrue: any;
ifFalse: any;
},
opts: { em: EditorModel; onValueChange?: () => void },
) {
if (typeof condition === 'undefined') {
throw new MissingConditionError();
if (isUndefined(props.condition)) {
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({
type: DataConditionType,
...props,
condition: conditionInstance,
ifTrue,
ifFalse,
});
this.em = opts.em;
this.lastEvaluationResult = this.evaluate();
this.listenToDataVariables();
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')!;
}
evaluate() {
return this.condition.evaluate();
getCondition(): ConditionProps {
return this.get('condition')?.get('condition')!;
}
getIfTrue() {
return this.get('ifTrue')!;
}
getIfFalse() {
return this.get('ifFalse')!;
}
getDataValue(): any {
return this.lastEvaluationResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em);
isTrue(): boolean {
return this.conditionEvaluator.evaluate();
}
reevaluate(): void {
this.lastEvaluationResult = this.evaluate();
getDataValue(skipDynamicValueResolution: boolean = false): any {
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) {
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() {
@ -97,7 +131,6 @@ export class DataCondition extends Model<DataConditionPropsDefined> {
em,
resolver: new DataVariable(variable, { em: this.em }),
onUpdate: (() => {
this.reevaluate();
this._onValueChange?.();
}).bind(this),
});
@ -107,9 +140,11 @@ export class DataCondition extends Model<DataConditionPropsDefined> {
}
getDependentDataVariables() {
const dataVariables: DataVariableProps[] = this.condition.getDataVariables();
if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue);
if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse);
const dataVariables: DataVariableProps[] = this.conditionEvaluator.getDependentDataVariables();
const ifTrue = this.get('ifTrue');
const ifFalse = this.get('ifFalse');
if (isDataVariable(ifTrue)) dataVariables.push(ifTrue);
if (isDataVariable(ifFalse)) dataVariables.push(ifFalse);
return dataVariables;
}
@ -120,16 +155,14 @@ export class DataCondition extends Model<DataConditionPropsDefined> {
}
toJSON() {
const ifTrue = this.get('ifTrue');
const ifFalse = this.get('ifFalse');
return {
type: DataConditionType,
condition: this.condition,
ifTrue: this.ifTrue,
ifFalse: this.ifFalse,
condition: this.conditionEvaluator,
ifTrue,
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 { evaluateVariable, isDataVariable } from '../utils';
import { ExpressionProps, LogicGroupProps } from './DataCondition';
import { LogicalGroupStatement } from './LogicalGroupStatement';
import { Operator } from './operators';
import { GenericOperation, GenericOperator } from './operators/GenericOperator';
import { LogicalOperator } from './operators/LogicalOperator';
import { LogicalGroupEvaluator } from './LogicalGroupEvaluator';
import { Operator } from './operators/BaseOperator';
import { AnyTypeOperation, AnyTypeOperator } from './operators/AnyTypeOperator';
import { BooleanOperator } from './operators/BooleanOperator';
import { NumberOperator, NumberOperation } from './operators/NumberOperator';
import { StringOperator, StringOperation } from './operators/StringOperations';
import { StringOperator, StringOperation } from './operators/StringOperator';
import { Model } from '../../../common';
import { DataConditionOperation } from './operators/types';
export type ConditionProps = ExpressionProps | LogicGroupProps | boolean;
export class Condition extends Model {
private condition: ConditionProps;
interface DataConditionEvaluatorProps {
condition: ConditionProps;
}
export class DataConditionEvaluator extends Model<DataConditionEvaluatorProps> {
private em: EditorModel;
constructor(props: ConditionProps, opts: { em: EditorModel }) {
constructor(props: DataConditionEvaluatorProps, opts: { em: EditorModel }) {
super(props);
this.condition = props;
this.em = opts.em;
}
evaluate(): boolean {
return this.evaluateCondition(this.condition);
}
/**
* Recursively evaluates conditions and logic groups.
*/
private evaluateCondition(condition: ConditionProps): boolean {
const em = this.em;
const condition = this.get('condition');
if (typeof condition === 'boolean') return condition;
if (this.isLogicGroup(condition)) {
const { logicalOperator, statements } = condition;
const operator = new LogicalOperator(logicalOperator);
const logicalGroup = new LogicalGroupStatement(operator, statements, { em: this.em });
const operator = new BooleanOperator(logicalOperator, { em });
const logicalGroup = new LogicalGroupEvaluator(operator, statements, { em });
return logicalGroup.evaluate();
}
@ -49,62 +47,65 @@ export class Condition extends Model {
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.
*/
private getOperator(left: any, operator: string): Operator {
if (this.isOperatorInEnum(operator, GenericOperation)) {
return new GenericOperator(operator as GenericOperation);
private getOperator(left: any, operator: string): Operator<DataConditionOperation> {
const em = this.em;
if (this.isOperatorInEnum(operator, AnyTypeOperation)) {
return new AnyTypeOperator(operator as AnyTypeOperation, { em });
} else if (typeof left === 'number') {
return new NumberOperator(operator as NumberOperation);
return new NumberOperator(operator as NumberOperation, { em });
} 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}`);
}
/**
* Extracts all data variables from the condition, including nested ones.
*/
getDataVariables() {
const variables: DataVariableProps[] = [];
this.extractVariables(this.condition, variables);
return variables;
getDependentDataVariables(): DataVariableProps[] {
const condition = this.get('condition');
if (!condition) return [];
return this.extractDataVariables(condition);
}
/**
* Recursively extracts variables from expressions or logic groups.
*/
private extractVariables(condition: ConditionProps, variables: DataVariableProps[]): void {
private extractDataVariables(condition: ConditionProps): DataVariableProps[] {
const variables: DataVariableProps[] = [];
if (this.isExpression(condition)) {
if (isDataVariable(condition.left)) variables.push(condition.left);
if (isDataVariable(condition.right)) variables.push(condition.right);
} 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 {
return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements);
}
/**
* Checks if a condition is an Expression.
*/
private isExpression(condition: any): condition is ExpressionProps {
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 {
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 { Condition, ConditionProps } from './Condition';
import { LogicalOperator } from './operators/LogicalOperator';
import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator';
import { BooleanOperator } from './operators/BooleanOperator';
export class LogicalGroupStatement {
export class LogicalGroupEvaluator {
private em: EditorModel;
constructor(
private operator: LogicalOperator,
private operator: BooleanOperator,
private statements: ConditionProps[],
opts: { em: EditorModel },
) {
@ -15,7 +15,7 @@ export class LogicalGroupStatement {
evaluate(): boolean {
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 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 { Operator } from '.';
import { Operator } from './BaseOperator';
import EditorModel from '../../../../editor/model/Editor';
export enum GenericOperation {
export enum AnyTypeOperation {
equals = 'equals',
isTruthy = 'isTruthy',
isFalsy = 'isFalsy',
@ -16,13 +17,9 @@ export enum GenericOperation {
isDefaultValue = 'isDefaultValue', // For Datasource variables
}
export class GenericOperator extends Operator {
constructor(private operator: GenericOperation) {
super();
}
export class AnyTypeOperator extends Operator<AnyTypeOperation> {
evaluate(left: any, right: any): boolean {
switch (this.operator) {
switch (this.operation) {
case 'equals':
return left === right;
case 'isTruthy':
@ -48,7 +45,8 @@ export class GenericOperator extends Operator {
case 'isDefaultValue':
return left instanceof DataVariable && left.get('defaultValue') === right;
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 {
greaterThan = '>',
@ -9,13 +9,9 @@ export enum NumberOperation {
notEquals = '!=',
}
export class NumberOperator extends Operator {
constructor(private operator: NumberOperation) {
super();
}
export class NumberOperator extends Operator<NumberOperation> {
evaluate(left: number, right: number): boolean {
switch (this.operator) {
switch (this.operation) {
case NumberOperation.greaterThan:
return left > right;
case NumberOperation.lessThan:
@ -29,7 +25,8 @@ export class NumberOperator extends Operator {
case NumberOperation.notEquals:
return left !== right;
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 {
contains = 'contains',
@ -9,13 +9,9 @@ export enum StringOperation {
trimEquals = 'trimEquals',
}
export class StringOperator extends Operator {
constructor(private operator: StringOperation) {
super();
}
export class StringOperator extends Operator<StringOperation> {
evaluate(left: string, right: string) {
switch (this.operator) {
switch (this.operation) {
case StringOperation.contains:
return left.includes(right);
case StringOperation.startsWith:
@ -23,14 +19,15 @@ export class StringOperator extends Operator {
case StringOperation.endsWith:
return left.endsWith(right);
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);
case StringOperation.equalsIgnoreCase:
return left.toLowerCase() === right.toLowerCase();
case StringOperation.trimEquals:
return left.trim() === right.trim();
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 Component from '../../../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types';
@ -17,12 +17,16 @@ import {
DataCollectionState,
DataCollectionStateMap,
} from './types';
import { getSymbolsToUpdate } from '../../../dom_components/model/SymbolUtils';
import { StyleProps, UpdateStyleOptions } from '../../../domain_abstract/model/StyleableModel';
import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers';
export default class ComponentDataCollection extends Component {
dataSourceWatcher?: DataResolverListener;
constructor(props: ComponentDataCollectionProps, opt: ComponentOptions) {
const collectionDef = props[keyCollectionDefinition];
// If we are cloning, leave setting the collection items to the main symbol collection
if (opt.forCloning) {
return super(props as any, opt) as unknown as ComponentDataCollection;
}
@ -36,129 +40,202 @@ export default class ComponentDataCollection extends Component {
return cmp;
}
const parentCollectionStateMap = (props[keyCollectionsStateMap] || {}) as DataCollectionStateMap;
const components: Component[] = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt);
cmp.components(components, opt);
this.rebuildChildrenFromCollection();
bindAll(this, 'rebuildChildrenFromCollection');
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)) {
this.watchDataSource(parentCollectionStateMap, opt);
setStartIndex(startIndex: number): void {
if (startIndex < 0) {
this.em.logError('Start index should be greater than or equal to 0');
return;
}
return cmp;
this.updateCollectionConfig({ startIndex });
}
get collectionConfig() {
return this.get(keyCollectionDefinition).collectionConfig as DataCollectionConfig;
setEndIndex(endIndex: number): void {
this.updateCollectionConfig({ endIndex });
}
get collectionDataSource() {
return this.collectionConfig.dataSource;
private updateCollectionConfig(updates: Partial<DataCollectionConfig>): void {
this.set(keyCollectionDefinition, {
...this.collectionDef,
collectionConfig: {
...this.collectionConfig,
...updates,
},
});
}
toJSON(opts?: ObjectAny) {
const json = super.toJSON.call(this, opts) as ComponentDataCollectionProps;
json[keyCollectionDefinition].componentDef = this.getComponentDef();
delete json.components;
delete json.droppable;
return json;
setDataSource(dataSource: DataCollectionDataSource) {
this.set(keyCollectionDefinition, {
...this.collectionDef,
collectionConfig: { ...this.collectionConfig, dataSource },
});
}
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 firstChildJSON = firstChild ? serialize(firstChild) : this.get(keyCollectionDefinition).componentDef;
const firstChildJSON = firstChild ? serialize(firstChild) : this.collectionDef.componentDef;
delete firstChildJSON?.draggable;
delete firstChildJSON?.removable;
return firstChildJSON;
}
private watchDataSource(parentCollectionStateMap: DataCollectionStateMap, opt: ComponentOptions) {
private listenToDataSource() {
const { em } = this;
const path = this.collectionDataSource?.path;
if (!path) return;
new DataResolverListener({
this.dataSourceWatcher = new DataResolverListener({
em,
resolver: new DataVariable({ type: DataVariableType, path }, { em }),
onUpdate: () => {
const collectionDef = { ...this.get(keyCollectionDefinition), componentDef: this.getComponentDef() };
const collectionItems = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt);
this.components().reset(collectionItems, updateFromWatcher as any);
},
onUpdate: this.rebuildChildrenFromCollection,
});
}
static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === DataCollectionType;
}
}
function getCollectionItems(
em: EditorModel,
collectionDef: DataCollectionProps,
parentCollectionStateMap: DataCollectionStateMap,
opt: ComponentOptions,
) {
const { componentDef, collectionConfig } = collectionDef;
const result = validateCollectionConfig(collectionConfig, componentDef, em);
if (!result) {
return [];
private rebuildChildrenFromCollection() {
this.components().reset(this.getCollectionItems(), updateFromWatcher as any);
}
const components: Component[] = [];
const collectionId = collectionConfig.collectionId;
const items = getDataSourceItems(collectionConfig.dataSource, 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),
};
getCollectionItems() {
const { componentDef, collectionConfig } = this.collectionDef;
const result = validateCollectionConfig(collectionConfig, componentDef, this.em);
if (parentCollectionStateMap[collectionId]) {
em.logError(
`The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`,
);
if (!result) {
return [];
}
const collectionsStateMap: DataCollectionStateMap = {
...parentCollectionStateMap,
[collectionId]: collectionState,
};
const components: Component[] = [];
const collectionId = collectionConfig.collectionId;
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 componentType = (componentDef?.type as string) || 'default';
let type = em.Components.getType(componentType) || em.Components.getType('default');
const Model = type.model;
symbolMain = new Model({ ...serialize(componentDef), draggable: false }, opt);
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(symbolMain);
const collectionsStateMap: DataCollectionStateMap = {
...parentCollectionStateMap,
[collectionId]: collectionState,
};
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 });
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance);
return components;
}
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(
collectionsStateMap: DataCollectionStateMap,
collectionId: string | undefined,
) {
function setCollectionStateMapAndPropagate(collectionsStateMap: DataCollectionStateMap, collectionId: string) {
return (cmp: Component) => {
setCollectionStateMap(collectionsStateMap)(cmp);
@ -169,7 +246,6 @@ function setCollectionStateMapAndPropagate(
const listenerKey = `_hasAddListener${collectionId ? `_${collectionId}` : ''}`;
const cmps = cmp.components();
// Add the 'add' listener if not already in the listeners array
if (!cmp.collectionStateListeners.includes(listenerKey)) {
cmp.listenTo(cmps, 'add', addListener);
cmp.collectionStateListeners.push(listenerKey);
@ -178,9 +254,15 @@ function setCollectionStateMapAndPropagate(
component.stopListening(component.components(), 'add', addListener);
component.off(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange);
const index = component.collectionStateListeners.indexOf(listenerKey);
if (index > -1) {
if (index !== -1) {
component.collectionStateListeners.splice(index, 1);
}
const collectionsStateMap = component.get(keyCollectionsStateMap);
component.set(keyCollectionsStateMap, {
...collectionsStateMap,
[collectionId]: undefined,
});
};
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;
}
@ -241,6 +329,28 @@ function setCollectionStateMap(collectionsStateMap: DataCollectionStateMap) {
};
cmp.set(keyCollectionsStateMap, 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;
items = listDataSourceVariables(id, em);
} else {
// Path points to a record in the data source
items = em.DataSources.getValue(dataSource.path, []);
}
break;

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

@ -18,6 +18,7 @@ export default class ComponentDataCollectionVariable extends Component {
collectionId: undefined,
variableType: 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 {
type: typeof DataCollectionType;
[keyCollectionDefinition]: DataCollectionProps;
}
@ -44,7 +45,6 @@ export interface ComponentDataCollectionVariableProps
Omit<ComponentProperties, 'type'> {}
export interface DataCollectionProps {
type: typeof DataCollectionType;
collectionConfig: DataCollectionConfig;
componentDef: ComponentDefinition;
}

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

@ -40,8 +40,7 @@ export function getDataResolverInstance(
resolver = new DataVariable(resolverProps, options);
break;
case DataConditionType: {
const { condition, ifTrue, ifFalse } = resolverProps;
resolver = new DataCondition(condition, ifTrue, ifFalse, options);
resolver = new DataCondition(resolverProps, options);
break;
}
case DataCollectionVariableType: {
@ -64,5 +63,5 @@ export function getDataResolverInstanceValue(
) {
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 attributesKeys = this.attributeWatcher.getDynamicValuesOfType(DataCollectionVariableType);
const combinedKeys = [keyCollectionsStateMap, ...keys];
const combinedKeys = [keyCollectionsStateMap, 'locked', ...keys];
const haveOverridenAttributes = Object.keys(attributesKeys).length;
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 { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
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 { DataResolverProps } from '../../data_sources/types';
import Component from './Component';
@ -90,7 +94,7 @@ export class ComponentResolverWatcher {
continue;
}
const { resolver } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap });
const resolver = getDataResolverInstance(resolverProps, { em, collectionsStateMap });
this.resolverListeners[key] = new DataResolverListener({
em,
resolver,
@ -112,8 +116,7 @@ export class ComponentResolverWatcher {
continue;
}
const { value } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap });
evaluatedValues[key] = value;
evaluatedValues[key] = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap });
}
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 { shallowDiff } from '../../utils/mixins';
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 CssRuleView from '../../css_composer/view/CssRuleView';
import ComponentView from '../../dom_components/view/ComponentView';
import Frame from '../../canvas/model/Frame';
import { DataConditionProps } from '../../data_sources/model/conditional_variables/DataCondition';
import {
DataCondition,
DataConditionType,
DataConditionProps,
} from '../../data_sources/model/conditional_variables/DataCondition';
import { isDataResolver, isDataResolverProps } from '../../data_sources/model/utils';
import { DataResolverProps } from '../../data_sources/types';
getDataResolverInstance,
getDataResolverInstanceValue,
isDataResolver,
isDataResolverProps,
} from '../../data_sources/model/utils';
import { DataResolver } from '../../data_sources/types';
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];
if (isDataResolverProps(styleValue)) {
const dataResolver = this.getDataResolverInstance(styleValue);
const dataResolver = getDataResolverInstance(styleValue, { em: this.em! });
if (dataResolver) {
newStyle[key] = dataResolver;
this.listenToDataResolver(dataResolver, key);
@ -141,25 +143,7 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
return newStyle;
}
private getDataResolverInstance(props: DataResolverProps) {
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) {
listenToDataResolver(resolver: DataResolver, styleProp: string) {
const resolverListener = this.styleResolverListeners[styleProp];
if (resolverListener) {
resolverListener.listenToResolver();
@ -203,10 +187,7 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
}
if (isDataResolverProps(styleValue)) {
const resolver = this.getDataResolverInstance(styleValue);
if (resolver) {
resultStyle[key] = resolver.getDataValue();
}
resultStyle[key] = getDataResolverInstanceValue(styleValue, { em: this.em! });
}
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 { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import {
MissingConditionError,
DataConditionType,
} from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
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';
@ -13,7 +10,7 @@ import ComponentTextView from '../../../../../src/dom_components/view/ComponentT
import EditorModel from '../../../../../src/editor/model/Editor';
import { setupTestEditor } from '../../../../common';
describe('ComponentConditionalVariable', () => {
describe('ComponentDataCondition', () => {
let editor: Editor;
let em: EditorModel;
let dsm: DataSourceManager;
@ -100,7 +97,7 @@ describe('ComponentConditionalVariable', () => {
type: DataVariableType,
path: 'ds1.left_id.left',
},
operator: GenericOperation.equals,
operator: AnyTypeOperation.equals,
right: {
type: DataVariableType,
path: 'ds1.right_id.right',
@ -124,10 +121,10 @@ describe('ComponentConditionalVariable', () => {
expect(childComponent.getInnerHTML()).toBe('Some value');
/* Test changing datasources */
updatedsmLeftValue(dsm, 'Diffirent value');
changeDataSourceValue(dsm, 'Diffirent value');
expect(getFirstChild(component).getInnerHTML()).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(getFirstChildView(component)?.el.innerHTML).toBe('Some value');
});
@ -149,7 +146,7 @@ describe('ComponentConditionalVariable', () => {
type: DataVariableType,
path: 'ds1.left_id.left',
},
operator: GenericOperation.equals,
operator: AnyTypeOperation.equals,
right: {
type: DataVariableType,
path: 'ds1.right_id.right',
@ -165,7 +162,7 @@ describe('ComponentConditionalVariable', () => {
type: DataVariableType,
path: 'ds1.left_id.left',
},
operator: GenericOperation.equals,
operator: AnyTypeOperation.equals,
right: {
type: DataVariableType,
path: 'ds1.right_id.right',
@ -213,24 +210,9 @@ describe('ComponentConditionalVariable', () => {
const storageCmptDef = frame.component.components[0];
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);
}

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

@ -1,10 +1,7 @@
import { DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import {
DataConditionType,
MissingConditionError,
} from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
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 ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
import EditorModel from '../../../../../src/editor/model/Editor';
@ -69,7 +66,7 @@ describe('StyleConditionalVariable', () => {
type: DataVariableType,
path: 'ds1.left_id.left',
},
operator: GenericOperation.equals,
operator: AnyTypeOperation.equals,
right: {
type: DataVariableType,
path: 'ds1.right_id.right',
@ -87,23 +84,6 @@ describe('StyleConditionalVariable', () => {
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', () => {
const conditionalStyleDef = {
tagName: 'h1',

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

@ -4,10 +4,10 @@ import {
ExpressionProps,
LogicGroupProps,
} from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
import { LogicalOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator';
import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator';
import { BooleanOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/BooleanOperator';
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 Editor from '../../../../../src/editor/model/Editor';
import EditorModel from '../../../../../src/editor/model/Editor';
@ -36,23 +36,41 @@ describe('DataCondition', () => {
describe('Basic Functionality Tests', () => {
test('should evaluate a simple boolean condition', () => {
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');
});
test('should return ifFalse when condition evaluates to 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');
});
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', () => {
test('should evaluate using GenericOperation operators', () => {
const condition: ExpressionProps = { left: 5, operator: GenericOperation.equals, right: 5 };
const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal', { em });
const condition: ExpressionProps = { left: 5, operator: AnyTypeOperation.equals, right: 5 };
const dataCondition = new DataCondition({ condition, ifTrue: 'Equal', ifFalse: 'Not Equal' }, { em });
expect(dataCondition.getDataValue()).toBe('Equal');
});
@ -60,63 +78,61 @@ describe('DataCondition', () => {
test('equals (false)', () => {
const condition: ExpressionProps = {
left: 'hello',
operator: GenericOperation.equals,
operator: AnyTypeOperation.equals,
right: 'world',
};
const dataCondition = new DataCondition(condition, 'true', 'false', { em });
expect(dataCondition.evaluate()).toBe(false);
const dataCondition = new DataCondition({ condition, ifTrue: 'true', ifFalse: 'false' }, { em });
expect(dataCondition.isTrue()).toBe(false);
});
test('should evaluate using StringOperation operators', () => {
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');
});
test('should evaluate using NumberOperation operators', () => {
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');
});
test('should evaluate using LogicalOperation operators', () => {
const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and,
logicalOperator: BooleanOperation.and,
statements: [
{ left: true, operator: GenericOperation.equals, right: true },
{ left: true, operator: AnyTypeOperation.equals, right: true },
{ 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');
});
});
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', () => {
const nestedLogicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.or,
logicalOperator: BooleanOperation.or,
statements: [
{
logicalOperator: LogicalOperation.and,
logicalOperator: BooleanOperation.and,
statements: [
{ 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 },
],
};
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');
});
});
@ -124,74 +140,89 @@ describe('DataCondition', () => {
describe('LogicalGroup Tests', () => {
test('should correctly handle AND logical operator', () => {
const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and,
logicalOperator: BooleanOperation.and,
statements: [
{ left: true, operator: GenericOperation.equals, right: true },
{ left: true, operator: AnyTypeOperation.equals, right: true },
{ 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');
});
test('should correctly handle OR logical operator', () => {
const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.or,
logicalOperator: BooleanOperation.or,
statements: [
{ left: true, operator: GenericOperation.equals, right: false },
{ left: true, operator: AnyTypeOperation.equals, right: false },
{ 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');
});
test('should correctly handle XOR logical operator', () => {
const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.xor,
logicalOperator: BooleanOperation.xor,
statements: [
{ left: true, operator: GenericOperation.equals, right: true },
{ left: true, operator: AnyTypeOperation.equals, right: true },
{ 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');
});
test('should handle nested logical groups', () => {
const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and,
logicalOperator: BooleanOperation.and,
statements: [
{ left: true, operator: GenericOperation.equals, right: true },
{ left: true, operator: AnyTypeOperation.equals, right: true },
{
logicalOperator: LogicalOperation.or,
logicalOperator: BooleanOperation.or,
statements: [
{ 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');
});
test('should handle groups with false conditions', () => {
const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and,
logicalOperator: BooleanOperation.and,
statements: [
{ left: true, operator: GenericOperation.equals, right: true },
{ left: false, operator: GenericOperation.equals, right: true },
{ left: true, operator: AnyTypeOperation.equals, right: true },
{ left: false, operator: AnyTypeOperation.equals, right: true },
{ 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');
});
});
@ -200,22 +231,22 @@ describe('DataCondition', () => {
test('should return "Yes" when dataVariable matches expected value', () => {
const condition: ExpressionProps = {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
operator: GenericOperation.equals,
operator: AnyTypeOperation.equals,
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');
});
test('should return "No" when dataVariable does not match expected value', () => {
const condition: ExpressionProps = {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
operator: GenericOperation.equals,
operator: AnyTypeOperation.equals,
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');
});
@ -223,11 +254,11 @@ describe('DataCondition', () => {
test.skip('should handle missing data variable gracefully', () => {
const condition: ExpressionProps = {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.not_a_user.status' },
operator: GenericOperation.isDefined,
operator: AnyTypeOperation.isDefined,
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');
});
@ -237,7 +268,7 @@ describe('DataCondition', () => {
operator: NumberOperation.greaterThan,
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');
});
@ -249,11 +280,11 @@ describe('DataCondition', () => {
dsm.add(dataSource2);
const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and,
logicalOperator: BooleanOperation.and,
statements: [
{
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
operator: GenericOperation.equals,
operator: AnyTypeOperation.equals,
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');
});
test('should handle nested logical conditions with data variables', () => {
const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.or,
logicalOperator: BooleanOperation.or,
statements: [
{
logicalOperator: LogicalOperation.and,
logicalOperator: BooleanOperation.and,
statements: [
{
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_2.status' },
operator: GenericOperation.equals,
operator: AnyTypeOperation.equals,
right: 'inactive',
},
{
@ -289,21 +323,26 @@ describe('DataCondition', () => {
},
{
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
operator: GenericOperation.equals,
operator: AnyTypeOperation.equals,
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');
});
test('should handle data variables as an ifTrue return value', () => {
const dataCondition = new DataCondition(
true,
{ type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
'No',
{
condition: true,
ifTrue: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
ifFalse: 'No',
},
{ em },
);
expect(dataCondition.getDataValue()).toBe('active');
@ -311,11 +350,14 @@ describe('DataCondition', () => {
test('should handle data variables as an ifFalse return value', () => {
const dataCondition = new DataCondition(
false,
'Yes',
{ type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
{
condition: false,
ifTrue: 'Yes',
ifFalse: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
},
{ em },
);
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 {
GenericOperator,
GenericOperation,
} from '../../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
AnyTypeOperator,
AnyTypeOperation,
} 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', () => {
let em: EditorModel;
beforeEach(() => {
em = new Editor();
});
afterEach(() => {
em.destroy();
});
describe('Operator: equals', () => {
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);
});
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);
});
});
describe('Operator: isTruthy', () => {
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);
});
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);
});
});
describe('Operator: isFalsy', () => {
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);
});
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);
});
});
describe('Operator: isDefined', () => {
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);
});
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);
});
});
describe('Operator: isNull', () => {
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);
});
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);
});
});
describe('Operator: isUndefined', () => {
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);
});
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);
});
});
describe('Operator: isArray', () => {
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);
});
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);
});
});
describe('Operator: isObject', () => {
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);
});
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);
});
});
describe('Operator: isString', () => {
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);
});
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);
});
});
describe('Operator: isNumber', () => {
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);
});
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);
});
});
describe('Operator: isBoolean', () => {
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);
});
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);
});
});
describe('Edge Case Tests', () => {
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);
});
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 {
LogicalOperator,
LogicalOperation,
} from '../../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator';
BooleanOperator,
BooleanOperation,
} 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', () => {
let em: EditorModel;
beforeEach(() => {
em = new Editor();
});
afterEach(() => {
em.destroy();
});
describe('Operator: and', () => {
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);
});
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);
});
});
describe('Operator: or', () => {
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);
});
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);
});
});
describe('Operator: xor', () => {
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);
});
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);
});
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);
});
});
describe('Edge Case Tests', () => {
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);
});
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,
NumberOperation,
} 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', () => {
let em: EditorModel;
beforeEach(() => {
em = new Editor();
});
afterEach(() => {
em.destroy();
});
describe('Operator: greaterThan', () => {
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);
});
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);
});
});
describe('Operator: lessThan', () => {
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);
});
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);
});
});
describe('Operator: greaterThanOrEqual', () => {
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);
});
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);
});
});
describe('Operator: lessThanOrEqual', () => {
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);
});
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);
});
});
describe('Operator: equals', () => {
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);
});
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);
});
});
describe('Operator: notEquals', () => {
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);
});
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);
});
});
describe('Edge Case Tests', () => {
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);
});
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);
});
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 {
StringOperator,
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', () => {
let em: EditorModel;
beforeEach(() => {
em = new Editor();
});
afterEach(() => {
em.destroy();
});
describe('Operator: contains', () => {
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);
});
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);
});
});
describe('Operator: startsWith', () => {
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);
});
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);
});
});
describe('Operator: endsWith', () => {
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);
});
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);
});
});
describe('Operator: matchesRegex', () => {
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);
});
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);
});
});
describe('Operator: equalsIgnoreCase', () => {
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);
});
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);
});
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('Hello', '')).toBe(false);
expect(operator.evaluate('', 'Hello')).toBe(false);
@ -73,17 +85,17 @@ describe('StringOperator', () => {
describe('Operator: trimEquals', () => {
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);
});
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);
});
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(' ', 'non-empty')).toBe(false);
});
@ -91,28 +103,23 @@ describe('StringOperator', () => {
describe('Edge Case Tests', () => {
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
});
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
});
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
});
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();
});
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