N°8772 - Form dependencies manager implementation

- Form SDK implementation
- Basic Forms
- Dynamics Forms
- Basic Blocks + Data Model Block
- Form Compilation
- Turbo integration
This commit is contained in:
Benjamin Dalsass
2025-12-30 11:42:55 +01:00
committed by GitHub
parent 3955b4eb22
commit 4c1ad0f4f2
813 changed files with 115243 additions and 489 deletions

View File

@@ -69,6 +69,7 @@
<file>../../core/apc-emulation.php</file>
<file>../../core/ormlinkset.class.inc.php</file>
<file>../../datamodels/2.x/itop-tickets/main.itop-tickets.php</file>
<directory>../../sources/Forms/</directory>
</whitelist>
</filter>

View File

@@ -10,9 +10,11 @@ namespace Combodo\iTop\Test\UnitTest;
use CMDBSource;
use DeprecatedCallsLog;
use MySQLTransactionNotClosedException;
use ParseError;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use ReflectionMethod;
use SetupUtils;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\ErrorHandler\Error\FatalError;
use Symfony\Component\HttpKernel\KernelInterface;
use const DEBUG_BACKTRACE_IGNORE_ARGS;
@@ -583,6 +585,23 @@ abstract class ItopTestCase extends KernelTestCase
self::assertLessThan(2, $iTimeInterval, $sMessage);
}
/**
* @since 3.3.0
*/
protected static function AssertPHPCodeIsValid(string $sPHPCode, $sMessage = ''): void
{
try {
eval($sPHPCode);
} catch (ParseError $e) {
$aLines = explode("\n", $sPHPCode);
foreach ($aLines as $iLine => $sLine) {
echo sprintf("%02d: %s\n", $iLine + 1, $sLine);
}
echo 'Parse Error: '.$e->getMessage().' at line: '.$e->getLine()."\n";
self::fail($sMessage);
}
}
/**
* Control which Kernel will be loaded when invoking the bootKernel method
*

View File

@@ -0,0 +1,72 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\sources\Forms;
use Combodo\iTop\Forms\Block\AbstractFormBlock;
use Combodo\iTop\Forms\Block\Base\FormBlock;
use Combodo\iTop\Forms\IO\Format\StringIOFormat;
use Combodo\iTop\Forms\IO\FormBlockIOException;
use Combodo\iTop\Forms\IO\FormInput;
use Combodo\iTop\Forms\IO\FormOutput;
use Combodo\iTop\Forms\Register\IORegister;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use ReflectionClass;
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
abstract class AbstractFormsTest extends ItopDataTestCase
{
/**
* @throws FormBlockIOException
*/
public function GivenInput(string $sName, string $sType = StringIOFormat::class): FormInput
{
$oBlock = $this->GivenFormBlock($sName);
$oInput = new FormInput($sName, $sType);
$oInput->SetOwnerBlock($oBlock);
return $oInput;
}
/**
* @throws FormBlockIOException
*/
public function GivenOutput(string $sName, string $sType = StringIOFormat::class): FormOutput
{
$oBlock = $this->GivenFormBlock($sName);
$oOutput = new FormOutput($sName, $sType);
$oOutput->SetOwnerBlock($oBlock);
return $oOutput;
}
public function GivenFormBlock(string $sName): FormBlock
{
return new FormBlock($sName, []);
}
public function GivenSubFormBlock(FormBlock $oParent, string $sName, string $ssBlockClass = FormBlock::class): AbstractFormBlock
{
$oParent->Add($sName, $ssBlockClass, []);
return $oParent->Get($sName);
}
public function GivenIORegister(AbstractFormBlock $oFormBlock): IORegister
{
$reflection = new ReflectionClass(AbstractFormBlock::class);
$reflection_property = $reflection->getProperty('oIORegister');
$reflection_property->setAccessible(true);
return $reflection_property->getValue($oFormBlock);
}
}

View File

@@ -0,0 +1,127 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Sources\Forms\Block;
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
use Combodo\iTop\Forms\Block\Base\CheckboxFormBlock;
use Combodo\iTop\Forms\Block\Base\FormBlock;
use Combodo\iTop\Forms\Block\Base\TextFormBlock;
use Combodo\iTop\Forms\Block\FormBlockException;
use Combodo\iTop\Forms\Block\IFormBlock;
use Combodo\iTop\Forms\Forms;
use Combodo\iTop\ItopSdkFormDemonstrator\Form\Block\Dashboard\GenericDashlet;
use Combodo\iTop\Service\InterfaceDiscovery\InterfaceDiscovery;
use Combodo\iTop\Test\UnitTest\sources\Forms\AbstractFormsTest;
use OutOfBoundsException;
use ReflectionException;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
/**
* Test forms block.
*
*/
class BlockTest extends AbstractFormsTest
{
/**
* Block get form type must return a class derived from Symfony form AbstractType.
*
* @throws ReflectionException
*/
public function testGetFormTypeReturnSymfonyType(): void
{
$aFormBlocks = InterfaceDiscovery::GetInstance()->FindItopClasses(iFormBlock::class);
foreach ($aFormBlocks as $sFormBlock) {
$oChoiceBlock = new ($sFormBlock)($sFormBlock);
if ($oChoiceBlock instanceof AbstractTypeFormBlock) {
if (!$oChoiceBlock instanceof GenericDashlet) {
$oClass = new \ReflectionClass($oChoiceBlock->GetFormType());
$this->assertTrue($oClass->isSubclassOf(AbstractType::class));
}
}
}
}
/**
* Pass a Symfony type instead of a FormBlock type will raise an exception
*
* @throws ReflectionException
*/
public function testAddChildBlockExpectFormBlockClass(): void
{
$oFormBlock = new FormBlock('formBlock');
$this->expectException(FormBlockException::class);
$oFormBlock->Add('wrong', TextType::class, []);
}
/**
* All block must contain a reference to themselves in their options
*/
public function testBlockOptionsContainsBlockReference(): void
{
$aFormBlocks = InterfaceDiscovery::GetInstance()->FindItopClasses(iFormBlock::class);
foreach ($aFormBlocks as $sFormBlock) {
$oChoiceBlock = new ($sFormBlock)($sFormBlock);
$this->assertTrue($oChoiceBlock->GetOption('form_block') === $oChoiceBlock);
}
}
/**
* Check that a block with dependencies return true for HasDependenciesBlocks.
*
* @return void
* @throws FormBlockException
* @throws ReflectionException
*/
public function testCheckDependencyState(): void
{
$oFormBlock = new FormBlock('formBlock');
$oFormBlock->Add('allow_age', CheckboxFormBlock::class, []);
$oBirthdateBlock = $oFormBlock->Add('birthdate', TextFormBlock::class, [])
->InputDependsOn(AbstractTypeFormBlock::INPUT_VISIBLE, 'allow_age', CheckboxFormBlock::OUTPUT_CHECKED);
$this->assertTrue($oBirthdateBlock->HasDependenciesBlocks());
}
/**
* Dependent fields are not added to the form directly.
*
* @return void
* @throws FormBlockException
* @throws ReflectionException
*/
public function testFormBlockNotContainsDependentFields(): void
{
// form with a dependent field
$oFormBlock = new FormBlock('formBlock');
$oFormBlock->Add('firstname', TextFormBlock::class, []);
$oFormBlock->Add('lastname', TextFormBlock::class, []);
$oFormBlock->Add('allow_age', CheckboxFormBlock::class, []);
$oFormBlock->Add('birthdate', TextFormBlock::class, [])
->InputDependsOn(AbstractTypeFormBlock::INPUT_VISIBLE, 'allow_age', CheckboxFormBlock::OUTPUT_CHECKED);
// form builder
$oFormFactoryBuilder = Forms::createFormFactoryBuilder();
$oForm = $oFormFactoryBuilder->getFormFactory()->createNamedBuilder($oFormBlock->GetName(), $oFormBlock->GetFormType(), [], $oFormBlock->GetOptions())->getForm();
// try to get the dependent field
$this->expectException(OutOfBoundsException::class);
$oForm->get('birthdate');
}
public function testIsRootBlock(): void
{
/** @var FormBlock $oFormBlock */
$oFormBlock = $this->GivenFormBlock('OneBlock');
$oFormBlock->Add('subform', FormBlock::class);
$this->assertTrue($oFormBlock->IsRootBlock());
$this->assertFalse($oFormBlock->Get('subform')->IsRootBlock());
}
}

View File

@@ -0,0 +1,751 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Forms\Compiler\FormsCompiler;
use Combodo\iTop\Service\DependencyInjection\DIService;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
class FormsCompilerTest extends ItopDataTestCase
{
/**
* @dataProvider CompileFormFromXMLProvider
*
* @param string $sXMLContent
* @param string $sExpectedPHP
*
* @return void
* @throws \Combodo\iTop\Forms\Compiler\FormsCompilerException
* @throws \Combodo\iTop\PropertyTree\PropertyTreeException
* @throws \DOMFormatException
*/
public function testCompileFormFromXML(string $sXMLContent, string $sExpectedPHP)
{
DIService::GetInstance()->RegisterService('ModelReflection', new ModelReflectionRuntime());
$sProducedPHP = FormsCompiler::GetInstance()->CompileFormFromXML($sXMLContent);
$this->AssertPHPCodeIsValid($sProducedPHP);
$sMessage = $this->dataName();
$this->assertEquals($sExpectedPHP, $sProducedPHP, $sMessage);
}
public function CompileFormFromXMLProvider()
{
return [
'Basic scalar properties should generate PHP' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<property_tree id="basic_test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="title_property">
<label>UI:BasicTest:Prop-Title</label>
<value-type xsi:type="Combodo-ValueType-Label">
</value-type>
</node>
<node id="class_property">
<label>UI:BasicTest:Prop-Class</label>
<value-type xsi:type="Combodo-ValueType-Class">
<categories-csv>test</categories-csv>
</value-type>
</node>
</nodes>
</property_tree>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__basic_test extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('title_property', 'Combodo\iTop\Forms\Block\DataModel\LabelFormBlock', [
'label' => 'UI:BasicTest:Prop-Title',
]);
\$this->Add('class_property', 'Combodo\iTop\Forms\Block\Base\ChoiceFormBlock', [
'label' => 'UI:BasicTest:Prop-Class',
'choices' => [
],
]);
}
}
PHP,
],
'Empty property tree should generate minimal PHP' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<property_tree id="EmptyTest" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
</nodes>
</property_tree>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__EmptyTest extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{ }
}
PHP,
],
'Empty property tree lower case should generate lower case class name' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<property_tree id="empty_test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
</nodes>
</property_tree>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__empty_test extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{ }
}
PHP,
],
'Properties with all value-types' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<property_tree id="AllValueTypesTest" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="aggregate_function_property">
<label>UI:AggregateFunction</label>
<value-type xsi:type="Combodo-ValueType-AggregateFunction">
</value-type>
</node>
<node id="choice_property">
<label>UI:Choice</label>
<value-type xsi:type="Combodo-ValueType-Choice">
<values>
<value id="value_a">
<label>Label A</label>
</value>
<value id="value_b">
<label>Label B</label>
</value>
</values>
</value-type>
</node>
<node id="class_property">
<label>UI:Class</label>
<value-type xsi:type="Combodo-ValueType-Class">
<categories-csv>test</categories-csv>
</value-type>
</node>
<node id="class_attribute_property">
<label>UI:ClassAttribute</label>
<value-type xsi:type="Combodo-ValueType-ClassAttribute">
</value-type>
</node>
<node id="class_attribute_group_by_property">
<label>UI:ClassAttributeGroupBy</label>
<value-type xsi:type="Combodo-ValueType-ClassAttributeGroupBy">
</value-type>
</node>
<node id="class_attribute_value_property">
<label>UI:ClassAttributeValue</label>
<value-type xsi:type="Combodo-ValueType-ClassAttributeValue">
</value-type>
</node>
<node id="integer_property">
<label>UI:Integer</label>
<value-type xsi:type="Combodo-ValueType-Integer">
</value-type>
</node>
<node id="label_property">
<label>UI:Label</label>
<value-type xsi:type="Combodo-ValueType-Label">
</value-type>
</node>
<node id="oql_property">
<label>UI:OQL</label>
<value-type xsi:type="Combodo-ValueType-OQL">
</value-type>
</node>
<node id="string_property">
<label>UI:String</label>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
<node id="choice_from_input">
<label>UI:ChoiceFromInput</label>
<value-type xsi:type="Combodo-ValueType-ChoiceFromInput">
<values>
<value id="value_a">
<label>{{class_attribute_property.label}}</label>
</value>
<value id="value_b">
<label>{{class_attribute_group_by_property.label}}</label>
</value>
</values>
</value-type>
</node>
</nodes>
</property_tree>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__AllValueTypesTest extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('aggregate_function_property', 'Combodo\iTop\Forms\Block\DataModel\Dashlet\AggregateFunctionFormBlock', [
'label' => 'UI:AggregateFunction',
]);
\$this->Add('choice_property', 'Combodo\iTop\Forms\Block\Base\ChoiceFormBlock', [
'label' => 'UI:Choice',
'choices' => [
\Dict::S('Label A') => 'value_a',
\Dict::S('Label B') => 'value_b',
],
]);
\$this->Add('class_property', 'Combodo\iTop\Forms\Block\Base\ChoiceFormBlock', [
'label' => 'UI:Class',
'choices' => [
],
]);
\$this->Add('class_attribute_property', 'Combodo\iTop\Forms\Block\DataModel\AttributeChoiceFormBlock', [
'label' => 'UI:ClassAttribute',
]);
\$this->Add('class_attribute_group_by_property', 'Combodo\iTop\Forms\Block\DataModel\Dashlet\ClassAttributeGroupByFormBlock', [
'label' => 'UI:ClassAttributeGroupBy',
]);
\$this->Add('class_attribute_value_property', 'Combodo\iTop\Forms\Block\DataModel\AttributeValueChoiceFormBlock', [
'label' => 'UI:ClassAttributeValue',
]);
\$this->Add('integer_property', 'Combodo\iTop\Forms\Block\Base\IntegerFormBlock', [
'label' => 'UI:Integer',
]);
\$this->Add('label_property', 'Combodo\iTop\Forms\Block\DataModel\LabelFormBlock', [
'label' => 'UI:Label',
]);
\$this->Add('oql_property', 'Combodo\iTop\Forms\Block\DataModel\OqlFormBlock', [
'label' => 'UI:OQL',
]);
\$this->Add('string_property', 'Combodo\iTop\Forms\Block\Base\TextFormBlock', [
'label' => 'UI:String',
]);
\$this->Add('choice_from_input', 'Combodo\iTop\Forms\Block\Base\ChoiceFromInputsBlock', [
'label' => 'UI:ChoiceFromInput',
])
->AddInputDependsOn('value_a', 'class_attribute_property', 'label')
->AddInputDependsOn('value_b', 'class_attribute_group_by_property', 'label');
}
}
PHP,
],
'Collection of trees' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<property_tree id="collection_of_trees_test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="sub_tree_collection">
<label>UI:SubTree</label>
<value-type xsi:type="Combodo-ValueType-Collection">
<prototype>
<node id="string_property">
<label>UI:String</label>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
<node id="integer_property">
<label>UI:Integer</label>
<relevance-condition>{{string_property.text != 'no-display'}}</relevance-condition>
<value-type xsi:type="Combodo-ValueType-Integer">
</value-type>
</node>
</prototype>
</value-type>
</node>
</nodes>
</property_tree>
XML,
'sExpectedPHP' => <<<PHP
class SubFormFor__collection_of_trees_test__sub_tree_collection extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('string_property', 'Combodo\iTop\Forms\Block\Base\TextFormBlock', [
'label' => 'UI:String',
]);
\$this->Add('integer_property_visible_expression', 'Combodo\iTop\Forms\Block\Expression\BooleanExpressionFormBlock', [
'expression' => 'string_property.text != \'no-display\'',
])
->AddInputDependsOn('string_property.text', 'string_property', 'text');
\$this->Add('integer_property', 'Combodo\iTop\Forms\Block\Base\IntegerFormBlock', [
'label' => 'UI:Integer',
])
->InputDependsOn('visible', 'integer_property_visible_expression', 'result');
}
}
class FormFor__collection_of_trees_test extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('sub_tree_collection', 'Combodo\iTop\Forms\Block\Base\CollectionBlock', [
'label' => 'UI:SubTree',
'button_label' => 'UI:AddSubTree',
'block_entry_type' => 'SubFormFor__collection_of_trees_test__sub_tree_collection',
]);
}
}
PHP,
],
'Static inputs should be bound and invalid input should be ignored' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="input_static_test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="class_attribute_property">
<label>UI:ClassAttribute</label>
<value-type xsi:type="Combodo-ValueType-ClassAttribute">
<class>Contact</class>
<invalid-input>Test</invalid-input>
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__input_static_test extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('class_attribute_property', 'Combodo\iTop\Forms\Block\DataModel\AttributeChoiceFormBlock', [
'label' => 'UI:ClassAttribute',
])
->SetInputValue('class', 'Contact');
}
}
PHP,
],
'Quotes should be handled gracefully' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="input_quotes_test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="class_attribute_property">
<label>'Class' and "Attribute"</label>
<value-type xsi:type="Combodo-ValueType-ClassAttribute">
<class>{{CONCAT("'", '"')}}</class>
<category>'Class' and "Attribute"</category>
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__input_quotes_test extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('class_attribute_property_class_expression', 'Combodo\iTop\Forms\Block\Expression\StringExpressionFormBlock', [
'expression' => 'CONCAT("\'", \'"\')',
]);
\$this->Add('class_attribute_property', 'Combodo\iTop\Forms\Block\DataModel\AttributeChoiceFormBlock', [
'label' => '\'Class\' and "Attribute"',
])
->InputDependsOn('class', 'class_attribute_property_class_expression', 'result')
->SetInputValue('category', '\'Class\' and "Attribute"');
}
}
PHP,
],
'Dynamic input should be bound' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="input_binding_test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="class_property">
<label>UI:Class</label>
<value-type xsi:type="Combodo-ValueType-Class">
<categories-csv>test</categories-csv>
</value-type>
</node>
<node id="class_attribute_property">
<label>UI:ClassAttribute</label>
<value-type xsi:type="Combodo-ValueType-ClassAttribute">
<class>{{class_property.text}}</class>
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__input_binding_test extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('class_property', 'Combodo\iTop\Forms\Block\Base\ChoiceFormBlock', [
'label' => 'UI:Class',
'choices' => [
],
]);
\$this->Add('class_attribute_property', 'Combodo\iTop\Forms\Block\DataModel\AttributeChoiceFormBlock', [
'label' => 'UI:ClassAttribute',
])
->InputDependsOn('class', 'class_property', 'text');
}
}
PHP,
],
'Dynamic input can be an expression' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="input_binding_expression" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="class_property">
<label>UI:Class</label>
<value-type xsi:type="Combodo-ValueType-Class">
<categories-csv>test</categories-csv>
</value-type>
</node>
<node id="class_attribute_property">
<label>UI:ClassAttribute</label>
<value-type xsi:type="Combodo-ValueType-ClassAttribute">
<class>{{IF(class_property.value = '', 'Person', class_property.value)}}</class>
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__input_binding_expression extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('class_property', 'Combodo\iTop\Forms\Block\Base\ChoiceFormBlock', [
'label' => 'UI:Class',
'choices' => [
],
]);
\$this->Add('class_attribute_property_class_expression', 'Combodo\iTop\Forms\Block\Expression\StringExpressionFormBlock', [
'expression' => 'IF(class_property.value = \'\', \'Person\', class_property.value)',
])
->AddInputDependsOn('class_property.value', 'class_property', 'value');
\$this->Add('class_attribute_property', 'Combodo\iTop\Forms\Block\DataModel\AttributeChoiceFormBlock', [
'label' => 'UI:ClassAttribute',
])
->InputDependsOn('class', 'class_attribute_property_class_expression', 'result');
}
}
PHP,
],
'Relevance condition should generate a boolean block expression' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="RelevanceCondition" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="source_property">
<label>UI:Source</label>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
<node id="dependant_property">
<label>UI:Dependant</label>
<relevance-condition>{{source_property.text != 'count'}}</relevance-condition>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__RelevanceCondition extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('source_property', 'Combodo\iTop\Forms\Block\Base\TextFormBlock', [
'label' => 'UI:Source',
]);
\$this->Add('dependant_property_visible_expression', 'Combodo\iTop\Forms\Block\Expression\BooleanExpressionFormBlock', [
'expression' => 'source_property.text != \'count\'',
])
->AddInputDependsOn('source_property.text', 'source_property', 'text');
\$this->Add('dependant_property', 'Combodo\iTop\Forms\Block\Base\TextFormBlock', [
'label' => 'UI:Dependant',
])
->InputDependsOn('visible', 'dependant_property_visible_expression', 'result');
}
}
PHP,
],
'Complex Relevance condition should generate a boolean block expression' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="ComplexRelevanceCondition" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="source_a_property">
<label>UI:Source</label>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
<node id="source_b_property">
<label>UI:Source</label>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
<node id="dependant_property">
<label>UI:Dependant</label>
<relevance-condition>{{IF(source_a_property.text != '', source_a_property.text, source_b_property.text)}}</relevance-condition>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__ComplexRelevanceCondition extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('source_a_property', 'Combodo\iTop\Forms\Block\Base\TextFormBlock', [
'label' => 'UI:Source',
]);
\$this->Add('source_b_property', 'Combodo\iTop\Forms\Block\Base\TextFormBlock', [
'label' => 'UI:Source',
]);
\$this->Add('dependant_property_visible_expression', 'Combodo\iTop\Forms\Block\Expression\BooleanExpressionFormBlock', [
'expression' => 'IF(source_a_property.text != \'\', source_a_property.text, source_b_property.text)',
])
->AddInputDependsOn('source_a_property.text', 'source_a_property', 'text')
->AddInputDependsOn('source_b_property.text', 'source_b_property', 'text');
\$this->Add('dependant_property', 'Combodo\iTop\Forms\Block\Base\TextFormBlock', [
'label' => 'UI:Dependant',
])
->InputDependsOn('visible', 'dependant_property_visible_expression', 'result');
}
}
PHP,
],
'Class category for value type class' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="ClassCategory" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="class_property">
<label>UI:Class</label>
<value-type xsi:type="Combodo-ValueType-Class">
<categories-csv>addon/authentication,grant_by_profile,silo</categories-csv>
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__ClassCategory extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{
\$this->Add('class_property', 'Combodo\iTop\Forms\Block\Base\ChoiceFormBlock', [
'label' => 'UI:Class',
'choices' => [
\Dict::S('Class:ActionEmail') => 'ActionEmail',
\Dict::S('Class:ActionNewsroom') => 'ActionNewsroom',
\Dict::S('Class:AuditCategory') => 'AuditCategory',
\Dict::S('Class:AuditDomain') => 'AuditDomain',
\Dict::S('Class:AuditRule') => 'AuditRule',
\Dict::S('Class:OAuthClientAzure') => 'OAuthClientAzure',
\Dict::S('Class:OAuthClientGoogle') => 'OAuthClientGoogle',
\Dict::S('Class:QueryOQL') => 'QueryOQL',
\Dict::S('Class:SynchroAttExtKey') => 'SynchroAttExtKey',
\Dict::S('Class:SynchroAttLinkSet') => 'SynchroAttLinkSet',
\Dict::S('Class:SynchroAttribute') => 'SynchroAttribute',
\Dict::S('Class:SynchroDataSource') => 'SynchroDataSource',
\Dict::S('Class:SynchroLog') => 'SynchroLog',
\Dict::S('Class:SynchroReplica') => 'SynchroReplica',
\Dict::S('Class:TriggerOnAttachmentCreate') => 'TriggerOnAttachmentCreate',
\Dict::S('Class:TriggerOnAttachmentDelete') => 'TriggerOnAttachmentDelete',
\Dict::S('Class:TriggerOnAttachmentDownload') => 'TriggerOnAttachmentDownload',
\Dict::S('Class:TriggerOnAttributeBlobDownload') => 'TriggerOnAttributeBlobDownload',
\Dict::S('Class:TriggerOnObjectCreate') => 'TriggerOnObjectCreate',
\Dict::S('Class:TriggerOnObjectDelete') => 'TriggerOnObjectDelete',
\Dict::S('Class:TriggerOnObjectMention') => 'TriggerOnObjectMention',
\Dict::S('Class:TriggerOnObjectUpdate') => 'TriggerOnObjectUpdate',
\Dict::S('Class:TriggerOnPortalUpdate') => 'TriggerOnPortalUpdate',
\Dict::S('Class:TriggerOnStateEnter') => 'TriggerOnStateEnter',
\Dict::S('Class:TriggerOnStateLeave') => 'TriggerOnStateLeave',
\Dict::S('Class:TriggerOnThresholdReached') => 'TriggerOnThresholdReached',
\Dict::S('Class:URP_Profiles') => 'URP_Profiles',
\Dict::S('Class:URP_UserOrg') => 'URP_UserOrg',
\Dict::S('Class:UserExternal') => 'UserExternal',
\Dict::S('Class:UserLDAP') => 'UserLDAP',
\Dict::S('Class:UserLocal') => 'UserLocal',
],
]);
}
}
PHP,
],
'test' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="Test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
</nodes>
</node>
XML,
'sExpectedPHP' => <<<PHP
class FormFor__Test extends Combodo\iTop\Forms\Block\Base\FormBlock
{
protected function BuildForm(): void
{ }
}
PHP,
],
];
}
/**
* @dataProvider CompileFormFromInvalidXMLProvider
* @param string $sXMLContent
* @param string $sExpectedClass
* @param string $sExpectedMessage
*
* @return void
* @throws \Combodo\iTop\Forms\Compiler\FormsCompilerException
* @throws \Combodo\iTop\PropertyTree\PropertyTreeException
* @throws \DOMFormatException
*/
public function testCompileFormFromInvalidXML(string $sXMLContent, string $sExpectedClass, string $sExpectedMessage)
{
$this->expectException($sExpectedClass);
$this->expectExceptionMessage($sExpectedMessage);
FormsCompiler::GetInstance()->CompileFormFromXML($sXMLContent);
}
public function CompileFormFromInvalidXMLProvider()
{
return [
'Invalid OQL expression in condition' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="RelevanceCondition" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="dependant_property">
<label>UI:Dependant</label>
<relevance-condition>{{source_property.text == 'count'}}</relevance-condition>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedClass' => 'Combodo\iTop\PropertyTree\PropertyTreeException',
'sExpectedMessage' => 'Node: dependant_property, invalid syntax in condition: Unexpected token EQ - found \'=\' at 22 in \'source_property.text == \'count\'\'',
],
'Unknown source in relevance condition' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="RelevanceCondition" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="dependant_property">
<label>UI:Dependant</label>
<relevance-condition>{{source_property.text = 'count'}}</relevance-condition>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedClass' => 'Combodo\iTop\PropertyTree\PropertyTreeException',
'sExpectedMessage' => 'Node: dependant_property, invalid source in condition: source_property',
],
'Unknown output in relevance condition' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="RelevanceCondition" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="source_property">
<label>UI:Source</label>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
<node id="dependant_property">
<label>UI:Dependant</label>
<relevance-condition>{{source_property.text_output != 'count'}}</relevance-condition>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedClass' => 'Combodo\iTop\PropertyTree\PropertyTreeException',
'sExpectedMessage' => 'Node: dependant_property, invalid output in condition: source_property.text_output',
],
'Missing output or source in relevance condition' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="RelevanceCondition" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="source_property">
<label>UI:Source</label>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
<node id="dependant_property">
<label>UI:Dependant</label>
<relevance-condition>{{source_property != 'count'}}</relevance-condition>
<value-type xsi:type="Combodo-ValueType-String">
</value-type>
</node>
</nodes>
</node>
XML,
'sExpectedClass' => 'Combodo\iTop\PropertyTree\PropertyTreeException',
'sExpectedMessage' => 'Node: dependant_property, missing output or source in condition: source_property',
],
'Missing value-type in node specification' => [
'sXMLContent' => <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<node id="RelevanceCondition" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Combodo-PropertyTree" xsi:noNamespaceSchemaLocation = "https://www.combodo.com/itop-schema/3.3">
<nodes>
<node id="source_property">
<label>UI:Source</label>
</node>
</nodes>
</node>
XML,
'sExpectedClass' => 'Combodo\iTop\PropertyTree\PropertyTreeException',
'sExpectedMessage' => 'Node: source_property, missing value-type in node specification',
],
];
}
}

View File

@@ -0,0 +1,108 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\sources\Forms\IO;
use Combodo\iTop\Forms\IO\FormBlockIOException;
use Combodo\iTop\Test\UnitTest\sources\Forms\AbstractFormsTest;
use Symfony\Component\Form\FormEvents;
class AbstractFormIOTest extends AbstractFormsTest
{
public function testFormIoHasNoDataAtCreation()
{
$oInput = $this->GivenInput('test');
$this->assertFalse($oInput->IsDataReady(), 'Created Input must no have data ready at creation');
$this->assertFalse($oInput->HasValue(), 'Created Input must no have value at creation');
$this->assertFalse($oInput->HasBindingOut());
$oOutput = $this->GivenOutput('test');
$this->assertFalse($oOutput->IsDataReady(), 'Created output must no have data ready at creation');
$this->assertFalse($oOutput->HasValue(), 'Created output must no have value at creation');
$this->assertFalse($oOutput->HasBindingOut());
}
public function testFormIoHasDataAfterSetValue()
{
$oInput = $this->GivenInput('test');
$oInput->SetValue(FormEvents::POST_SET_DATA, 'test');
$this->assertTrue($oInput->IsDataReady(), 'Input must have data ready when set');
$this->assertTrue($oInput->HasValue(), 'Input must have value when set');
$oOutput = $this->GivenOutput('test');
$oOutput->SetValue(FormEvents::POST_SET_DATA, 'test');
$this->assertTrue($oOutput->IsDataReady(), 'Output must have data ready when set');
$this->assertTrue($oOutput->HasValue(), 'Output must have value when set');
}
public function testIOValueReflectsTheValuePostedOrTheValueSet()
{
$oInput = $this->GivenInput('test');
// When
$oInput->SetValue(FormEvents::POST_SET_DATA, 'The value set');
// Then
$this->assertEquals('The value set', $oInput->GetValue());
// When
$oInput->SetValue(FormEvents::POST_SUBMIT, 'The value posted');
// Then
$this->assertEquals('The value posted', $oInput->GetValue());
}
/**
* @dataProvider NameFormatSupportsOnlyLettersUnderscoreAndNumbersProvider
* @return void
* @throws \Combodo\iTop\Forms\IO\FormBlockIOException
*/
public function testNameFormatSupportsOnlyLettersUnderscoreAndNumbersAndDot(string $sName, bool $bGenerateException = true)
{
if ($bGenerateException) {
$this->expectException(FormBlockIOException::class);
}
$oInput = $this->GivenInput($sName);
if (!$bGenerateException) {
$this->assertEquals($sName, $oInput->GetName());
}
}
public function NameFormatSupportsOnlyLettersUnderscoreAndNumbersProvider()
{
return [
// Incorrects
'Spaces not supported' => ['The test name'],
'Minus not supported' => ['The-test-name'],
'Percent not supported' => ['name%'],
'Accent not supported' => ['namé'],
'emoji not supported' => ['🎄🎄🎄🎄🎄'],
'.name not supported' => ['.name'],
'name. not supported' => ['name.'],
// Corrects
'Numbers OK' => ['name123', false],
'Starting with number OK' => ['123name123', false],
'Underscore OK' => ['The_test_name', false],
'Camel OK' => ['TheTestName', false],
'name.subname OK' => ['name.subname', false],
];
}
public function testCreatingIOWithUnknownFormatThrowsException()
{
$this->expectException(FormBlockIOException::class);
$oInput = $this->GivenInput('test', 'test_toto');
}
}

View File

@@ -0,0 +1,237 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Sources\Forms\IO;
use Combodo\iTop\Forms\IO\Format\AttributeIOFormat;
use Combodo\iTop\Forms\IO\Format\BooleanIOFormat;
use Combodo\iTop\Forms\IO\Format\ClassIOFormat;
use Combodo\iTop\Forms\IO\Format\NumberIOFormat;
use Combodo\iTop\Forms\IO\Format\StringIOFormat;
use Combodo\iTop\Forms\IO\FormBinding;
use Combodo\iTop\Forms\IO\FormBlockIOException;
use Combodo\iTop\Test\UnitTest\sources\Forms\AbstractFormsTest;
use Symfony\Component\Form\FormEvents;
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
class FormBindingTest extends AbstractFormsTest
{
public function testCreatingABinding()
{
$oInputIO = $this->GivenInput('test');
$oOutputIO = $this->GivenOutput('test');
// When Linking output to input
new FormBinding($oOutputIO, $oInputIO);
// Then
$this->assertTrue($oInputIO->IsBound(), 'DestinationIO must be Bound when creating a new binding');
}
public function testBindingTwiceToTheSameInputIsNotPossible()
{
$oInputIO = $this->GivenInput('test');
$oOutputIO1 = $this->GivenOutput('test1');
$oOutputIO2 = $this->GivenOutput('test2');
// When
new FormBinding($oOutputIO1, $oInputIO);
// Then
$this->expectException(FormBlockIOException::class);
new FormBinding($oOutputIO2, $oInputIO);
}
public function testBindingTwiceToTheSameOutputIsNotPossible()
{
$oOutputIO1 = $this->GivenOutput('test1');
$oOutputIO2 = $this->GivenOutput('test2');
$oOutputIO3 = $this->GivenOutput('test3');
// When
new FormBinding($oOutputIO1, $oOutputIO3);
// Then
$this->expectException(FormBlockIOException::class);
new FormBinding($oOutputIO2, $oOutputIO3);
}
public function testOutputCanBeBoundToInputAndInputIsBoundAfterThat()
{
$oInputIO = $this->GivenInput('test');
$oOutputIO = $this->GivenOutput('test1');
$this->assertFalse($oInputIO->IsBound(), 'Input must not be Bound by default');
// When
$oOutputIO->BindToInput($oInputIO);
// Then
$this->assertTrue($oInputIO->IsBound(), 'Input must be Bound when binding from an output');
}
public function testInputCanBeBoundToAnotherInputAndItIsBoundAfterThat()
{
$oInputIO1 = $this->GivenInput('test1');
$oInputIO2 = $this->GivenInput('test2');
// When
$oInputIO1->BindToInput($oInputIO2);
// Then
$this->assertTrue($oInputIO2->IsBound(), 'Input must be Bound when binding from an input');
}
public function testOutputCanBeBoundToAnotherOutputAndItIsBoundAfterThat()
{
$oOutputIO1 = $this->GivenOutput('test1');
$oOutputIO2 = $this->GivenOutput('test2');
$this->assertFalse($oOutputIO2->IsBound(), 'Output must not be bound by default');
// When
$oOutputIO1->BindToOutput($oOutputIO2);
// Then
$this->assertTrue($oOutputIO2->IsBound(), 'Output must be Bound when binding from an output');
}
public function testOutBindingsAreStoredWhenBindToInput()
{
$oInputIO1 = $this->GivenInput('test1');
$oInputIO2 = $this->GivenInput('test2');
$oOutputIO1 = $this->GivenOutput('test1');
// When
$oBindingO2ToI1 = $oOutputIO1->BindToInput($oInputIO1);
// Then
$this->assertTrue($oOutputIO1->HasBindingOut(), 'Must have bindings after BindToInput');
$this->assertEquals([$oBindingO2ToI1], $oOutputIO1->GetBindingsToInputs(), 'Must have bindings after BindToInput');
// When
$oBindingO1ToI2 = $oOutputIO1->BindToInput($oInputIO2);
// Then
$this->assertEquals([$oBindingO2ToI1, $oBindingO1ToI2], $oOutputIO1->GetBindingsToInputs(), 'Must have bindings after BindToInput');
}
public function testOutBindingsAreStoredWhenBindToOutput()
{
$oOutputIO1 = $this->GivenOutput('test1');
$oOutputIO2 = $this->GivenOutput('test2');
$oOutputIO3 = $this->GivenOutput('test3');
// When
$oBindingO1ToO2 = $oOutputIO1->BindToOutput($oOutputIO2);
// Then
$this->assertTrue($oOutputIO1->HasBindingOut(), 'Must have bindings after BindToInput');
$this->assertEquals([$oBindingO1ToO2], $oOutputIO1->GetBindingsToOutputs(), 'Must have bindings after BindToOutput');
// When
$oBindingO1ToO3 = $oOutputIO1->BindToOutput($oOutputIO3);
// Then
$this->assertEquals([$oBindingO1ToO2, $oBindingO1ToO3], $oOutputIO1->GetBindingsToOutputs(), 'Must have bindings after BindToOutput');
}
public function testSourceValueIsPropagatedToDestIO()
{
$oOutputIO1 = $this->GivenOutput('test1');
$oInputIO1 = $this->GivenInput('test1');
$oBinding = $oOutputIO1->BindToInput($oInputIO1);
$oOutputIO1->SetValue(FormEvents::PRE_SET_DATA, 'The Value');
// When
$oBinding->PropagateValues();
// Then
$this->assertEquals('The Value', $oOutputIO1->GetValue(FormEvents::PRE_SET_DATA));
}
/**
* @dataProvider BindingIncompatibleFormatsProvider
*
* @param string $sSourceFormat
* @param string $sDestinationFormat
*
* @return void
*/
public function testBindingIncompatibleFormatsThrowsException(string $sSourceFormat, string $sDestinationFormat)
{
$oOutputIO = $this->GivenOutput('test', $sSourceFormat);
$oInputIO = $this->GivenInput('test', $sDestinationFormat);
$this->expectException(FormBlockIOException::class);
$oOutputIO->BindToInput($oInputIO);
}
public function BindingIncompatibleFormatsProvider(): array
{
return [
'Attribute -> Boolean' => [AttributeIOFormat::class, BooleanIOFormat::class],
'Attribute -> Class' => [AttributeIOFormat::class, ClassIOFormat::class],
'Attribute -> Number' => [AttributeIOFormat::class, NumberIOFormat::class],
'Attribute -> String' => [AttributeIOFormat::class, StringIOFormat::class],
'Boolean => Attribute' => [BooleanIOFormat::class, AttributeIOFormat::class],
'Boolean => Class' => [BooleanIOFormat::class, ClassIOFormat::class],
'Boolean => Number' => [BooleanIOFormat::class, NumberIOFormat::class],
'Boolean -> String' => [BooleanIOFormat::class, StringIOFormat::class],
'Class => Attribute' => [ClassIOFormat::class, AttributeIOFormat::class],
'Class => Boolean' => [ClassIOFormat::class, BooleanIOFormat::class],
'Class => Number' => [ClassIOFormat::class, NumberIOFormat::class],
'Class -> String' => [ClassIOFormat::class, StringIOFormat::class],
'Number => Attribute' => [NumberIOFormat::class, AttributeIOFormat::class],
'Number => Class' => [NumberIOFormat::class, ClassIOFormat::class],
'Number => Boolean' => [NumberIOFormat::class, BooleanIOFormat::class],
'Number -> String' => [NumberIOFormat::class, StringIOFormat::class],
'String => Attribute' => [StringIOFormat::class, AttributeIOFormat::class],
'String => Class' => [StringIOFormat::class, ClassIOFormat::class],
'String => Boolean' => [StringIOFormat::class, BooleanIOFormat::class],
'String -> Number' => [StringIOFormat::class, NumberIOFormat::class],
];
}
/**
* @dataProvider BindingCompatibleFormatsProvider
*
* @param string $sSourceFormat
* @param string $sDestinationFormat
*
* @return void
*/
public function testBindingCompatibleFormatsWorks(string $sSourceFormat, string $sDestinationFormat)
{
$oOutputIO = $this->GivenOutput('test', $sSourceFormat);
$oInputIO = $this->GivenInput('test', $sDestinationFormat);
$oBinding = $oOutputIO->BindToInput($oInputIO);
$this->assertTrue(is_a($oBinding, FormBinding::class));
}
public function BindingCompatibleFormatsProvider(): array
{
return [
'Attribute -> Attribute' => [AttributeIOFormat::class, AttributeIOFormat::class],
'Boolean => Boolean' => [BooleanIOFormat::class, BooleanIOFormat::class],
'Class => Class' => [ClassIOFormat::class, ClassIOFormat::class],
'Number => Number' => [NumberIOFormat::class, NumberIOFormat::class],
'String => String' => [StringIOFormat::class, StringIOFormat::class],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\sources\Forms\IO\Format;
use Combodo\iTop\Forms\IO\Format\AttributeIOFormat;
use Combodo\iTop\Test\UnitTest\sources\Forms\AbstractFormsTest;
use Symfony\Component\Form\FormEvents;
class TestAttributeIOFormat extends AbstractFormsTest
{
public function testAttributeIOIsAString()
{
$oInputIO = $this->GivenInput('test', AttributeIOFormat::class);
$oInputIO->SetValue(FormEvents::POST_SUBMIT, 'name');
$this->assertEquals('name', $oInputIO->GetValue());
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\sources\Forms\IO\Format;
use Combodo\iTop\Forms\IO\Format\BooleanIOFormat;
use Combodo\iTop\Test\UnitTest\sources\Forms\AbstractFormsTest;
use Symfony\Component\Form\FormEvents;
class TestBooleanIOFormat extends AbstractFormsTest
{
public function testBooleanIOFormatIsABoolean()
{
$oInputIO = $this->GivenInput('test', BooleanIOFormat::class);
$oInputIO->SetValue(FormEvents::POST_SUBMIT, 'true');
$this->assertEquals(true, $oInputIO->GetValue());
}
}

View File

@@ -0,0 +1,14 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\sources\Forms\IO\Format;
use Combodo\iTop\Test\UnitTest\sources\Forms\AbstractFormsTest;
class TestClassIOFormat extends AbstractFormsTest
{
}

View File

@@ -0,0 +1,14 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\sources\Forms\IO\Format;
use Combodo\iTop\Test\UnitTest\sources\Forms\AbstractFormsTest;
class TestNumberIOFormat extends AbstractFormsTest
{
}

View File

@@ -0,0 +1,193 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Sources\Forms\Register;
use Combodo\iTop\Forms\Block\Base\CheckboxFormBlock;
use Combodo\iTop\Forms\Block\Base\FormBlock;
use Combodo\iTop\Forms\Block\Base\TextFormBlock;
use Combodo\iTop\Forms\IO\Format\BooleanIOFormat;
use Combodo\iTop\Forms\IO\Format\StringIOFormat;
use Combodo\iTop\Forms\Register\IORegister;
use Combodo\iTop\Forms\Register\RegisterException;
use Combodo\iTop\Test\UnitTest\sources\Forms\AbstractFormsTest;
class IORegisterTest extends AbstractFormsTest
{
private FormBlock $oFormBlock;
private IORegister $oIORegister;
protected function setUp(): void
{
parent::setUp();
$this->oFormBlock = $this->GivenFormBlock('OneBlock');
$this->oIORegister = $this->GivenIORegister($this->oFormBlock);
}
public function testAddInput(): void
{
$this->oIORegister->AddInput('input', StringIOFormat::class);
$this->assertTrue($this->oIORegister->HasInput('input'));
$this->assertNotNull($this->oIORegister->GetInput('input'));
}
public function testGetInputs(): void
{
$iOriginInputCount = count($this->oIORegister->GetInputs());
$this->oIORegister->AddInput('input_1', StringIOFormat::class);
$this->oIORegister->AddInput('input_2', BooleanIOFormat::class);
$this->oIORegister->AddInput('input_3', StringIOFormat::class);
$this->assertCount(3 + $iOriginInputCount, $this->oIORegister->GetInputs());
$this->assertArrayHasKey('input_1', $this->oIORegister->GetInputs());
}
public function testGetOutputs(): void
{
$this->oIORegister->AddOutput('output_1', StringIOFormat::class);
$this->oIORegister->AddOutput('output_2', BooleanIOFormat::class);
$this->assertCount(2, $this->oIORegister->GetOutputs());
$this->assertArrayHasKey('output_1', $this->oIORegister->GetOutputs());
}
public function testMissingInput(): void
{
$this->expectException(RegisterException::class);
$this->oIORegister->GetInput('missing_input');
}
public function testMissingOutput(): void
{
$this->expectException(RegisterException::class);
$this->oIORegister->GetOutput('missing_output');
}
public function testGetBoundInputs(): void
{
$this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class);
$this->GivenSubFormBlock($this->oFormBlock, 'SubFormB', CheckboxFormBlock::class);
$this->GivenSubFormBlock($this->oFormBlock, 'SubFormC', TextFormBlock::class);
$oSubForm = $this->GivenSubFormBlock($this->oFormBlock, 'SubForm', TextFormBlock::class);
$oSubForm->AddInput('input_from_A', StringIOFormat::class);
$oSubForm->AddInput('input_from_B', BooleanIOFormat::class);
$oSubForm->AddInput('input_from_C', StringIOFormat::class);
$oSubForm->AddInput('unbound_input', StringIOFormat::class);
$this->GivenIORegister($oSubForm)->InputDependsOn('input_from_A', 'SubFormA', TextFormBlock::OUTPUT_TEXT);
$this->GivenIORegister($oSubForm)->InputDependsOn('input_from_B', 'SubFormB', CheckboxFormBlock::OUTPUT_CHECKED);
$this->GivenIORegister($oSubForm)->InputDependsOn('input_from_C', 'SubFormC', TextFormBlock::OUTPUT_TEXT);
$aBoundInputs = $this->GivenIORegister($oSubForm)->GetBoundInputs();
$this->assertCount(3, $aBoundInputs);
}
public function testGetBoundOutputs(): void
{
$this->oFormBlock->AddOutput('output', StringIOFormat::class);
$oSubFormA = $this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class);
$oIORegisterA = $this->GivenIORegister($oSubFormA);
$oIORegisterA->OutputImpactParent(TextFormBlock::OUTPUT_TEXT, 'output');
$this->assertCount(1, $this->oIORegister->GetBoundOutputs());
}
public function testAddInputDependsOn(): void
{
$this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class);
$oSubFormB = $this->GivenSubFormBlock($this->oFormBlock, 'SubFormB', TextFormBlock::class);
$oIORegisterB = $this->GivenIORegister($oSubFormB);
$oIORegisterB->AddInputDependsOn('input', 'SubFormA', TextFormBlock::OUTPUT_TEXT);
$this->assertNotNull($oIORegisterB->GetInput('input'));
}
public function testImpactParent(): void
{
$this->oFormBlock->AddOutput('output', StringIOFormat::class);
$oSubFormA = $this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class);
$oIORegisterA = $this->GivenIORegister($oSubFormA);
$oIORegisterA->OutputImpactParent(TextFormBlock::OUTPUT_TEXT, 'output');
$this->assertTrue($this->oFormBlock->GetOutput('output')->IsBound());
}
public function testAddingTwiceTheSameInputThrowsException(): void
{
$this->oIORegister->AddInput('test_input', StringIOFormat::class);
$this->expectException(RegisterException::class);
$this->oIORegister->AddInput('test_input', StringIOFormat::class);
}
public function testAddingTwiceTheSameOutputThrowsException(): void
{
$this->oIORegister->AddOutput('test_output', StringIOFormat::class);
$this->expectException(RegisterException::class);
$this->oIORegister->AddOutput('test_output', StringIOFormat::class);
}
public function testDependingOnNonExistingInputThrowsException(): void
{
$this->oIORegister->AddInput('test_input', StringIOFormat::class);
$this->oIORegister->AddOutput('test_output', StringIOFormat::class);
$this->expectException(RegisterException::class);
$this->oIORegister->InputDependsOn('non_existing_input', 'OtherBlock', 'test_output');
}
public function testDependingOnNonExistingOutputThrowsException(): void
{
$this->oIORegister->AddInput('test_input', StringIOFormat::class);
$this->expectException(RegisterException::class);
$this->oIORegister->InputDependsOn('test_input', 'OtherBlock', 'non_existing_output');
}
public function testDependingOnNonExistingBlockThrowsException(): void
{
$this->oIORegister->AddInput('test_input', StringIOFormat::class);
$this->oIORegister->AddOutput('test_output', StringIOFormat::class);
$this->expectException(RegisterException::class);
$this->oIORegister->InputDependsOn('test_input', 'UnknownBlock', 'test');
}
public function testHasDependenciesBlocks(): void
{
$this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class);
$oSubForm = $this->GivenSubFormBlock($this->oFormBlock, 'SubForm', TextFormBlock::class);
$oSubForm->AddInput('input_from_A', StringIOFormat::class);
$this->GivenIORegister($oSubForm)->InputDependsOn('input_from_A', 'SubFormA', TextFormBlock::OUTPUT_TEXT);
$this->assertTrue($this->GivenIORegister($oSubForm)->HasDependenciesBlocks());
$this->assertFalse($this->oIORegister->HasDependenciesBlocks());
}
public function testImpactBlocks(): void
{
$oSubFormA = $this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class);
$oSubForm = $this->GivenSubFormBlock($this->oFormBlock, 'SubForm', TextFormBlock::class);
$oSubForm->AddInput('input_from_A', StringIOFormat::class);
$this->GivenIORegister($oSubForm)->InputDependsOn('input_from_A', 'SubFormA', TextFormBlock::OUTPUT_TEXT);
$this->assertFalse($this->GivenIORegister($oSubForm)->IsImpactingBlocks());
$this->assertTrue($this->GivenIORegister($oSubFormA)->IsImpactingBlocks());
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Sources\Forms\Register;
use Combodo\iTop\Forms\Register\OptionsRegister;
use Combodo\iTop\Forms\Register\RegisterException;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
class OptionsRegisterTest extends ItopDataTestCase
{
private OptionsRegister $oOptionsRegister;
protected function setUp(): void
{
parent::setUp();
$this->oOptionsRegister = new OptionsRegister();
}
public function testSetOptionWithInvalidName(): void
{
$this->oOptionsRegister->SetOption('valid_option_name', 'value');
$this->expectException(RegisterException::class);
$this->oOptionsRegister->SetOption('not valid option name', 'value');
}
public function testSetOptionTwice(): void
{
$this->oOptionsRegister->SetOption('valid_option_name', 'value');
$this->oOptionsRegister->SetOption('valid_option_name', 'value2');
$this->assertEquals('value2', $this->oOptionsRegister->GetOption('valid_option_name'));
}
public function testSetNonTypeOption(): void
{
$this->oOptionsRegister->SetOption('not_a_type_option', 'value', false);
$this->assertArrayNotHasKey('not_a_type_option', $this->oOptionsRegister->GetOptions());
}
public function testSetOptionArrayValue(): void
{
$this->oOptionsRegister->SetOptionArrayValue('att', 'class', 'ibo-class');
$this->assertEquals('ibo-class', $this->oOptionsRegister->GetOption('att')['class']);
}
public function testHasOption(): void
{
$this->oOptionsRegister->SetOption('option', true);
$this->assertTrue($this->oOptionsRegister->HasOption('option'));
}
}

View File

@@ -156,10 +156,26 @@ class DataModelDependantCacheTest extends ItopTestCase
$this->assertEquals($iRefTime, $this->oCacheService->GetEntryModificationTime('pool-A', 'key'), 'GetEntryModificationTime should return the modification time of the cache file');
$this->assertEquals(null, $this->oCacheService->GetEntryModificationTime('pool-A', 'non-existing-key'), 'GetEntryModificationTime should return null for an invalid key');
}
public function testKeyUndesiredCharactersShouldBeTransformedToUnderscore()
{
$sUglyKey = 'key with ugly characters:\{&"#@ç^²/,;[(|🤔';
$sUglyKey = 'key with ugly characters:\{&"#@ç^²,;[(|🤔';
$sFilePath = $this->InvokeNonPublicMethod(DataModelDependantCache::class, 'MakeCacheFileName', $this->oCacheService, ['pool-A', $sUglyKey]);
$this->assertEquals('key_with_ugly_characters______________________.php', basename($sFilePath));
$this->assertEquals('key_with_ugly_characters_____________________.php', basename($sFilePath));
}
public function testKeyCanBeADirectoryTree()
{
$sBaseKey = 'test';
$sBaseFilePath = $this->InvokeNonPublicMethod(DataModelDependantCache::class, 'MakeCacheFileName', $this->oCacheService, ['pool-A', $sBaseKey]);
$sKey = 'Path/To/KeyCanBePath';
$sFilePath = $this->InvokeNonPublicMethod(DataModelDependantCache::class, 'MakeCacheFileName', $this->oCacheService, ['pool-A', $sKey]);
$this->assertEquals(dirname($sBaseFilePath).'/Path/To', dirname($sFilePath));
$this->oCacheService->Store('pool-A', $sKey, 'some data...');
$this->assertTrue($this->oCacheService->HasEntry('pool-A', $sKey), 'The data should have been stored');
$this->assertFileExists($sFilePath, 'A file should have been created in the corresponding directory');
}
}