mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 18:50:16 +03:00
ui: add typed value conversion errors
Replace boolean output-parameter converters with UIValueResult so conversion failures can provide numeric codes and optional diagnostics without exposing partially converted values. Expose observable converter error state from UIDataBind, UIProperty, and UIValueBinding. Reject invalid widget input without changing the model or propagating it to sibling widgets, while keeping programmatic model updates authoritative. Migrate ecode's custom output parser converter, expand binding tests, and document the data-handling architecture and next form-validation phase.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/ui/uicheckbox.hpp>
|
||||
#include <eepp/ui/uitextinput.hpp>
|
||||
#include <eepp/ui/uivaluebinding.hpp>
|
||||
|
||||
using namespace EE;
|
||||
@@ -90,3 +91,79 @@ UTEST( UIValueBinding, synchronizesBothDirectionsAndHandlesEndpointLifetimes ) {
|
||||
eeDelete( widget );
|
||||
EXPECT_FALSE( static_cast<bool>( binding ) );
|
||||
}
|
||||
|
||||
UTEST( UIValueBinding, validatesWidgetProposalsButAcceptsAuthoritativeObservableValues ) {
|
||||
UIApplication app(
|
||||
WindowSettings( 320, 240, "eepp - UIValueBinding Validation Test", WindowStyle::Default,
|
||||
WindowBackend::Default, 32 ),
|
||||
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
|
||||
auto widget = UITextInput::New();
|
||||
ObservableValue<std::string> value( "initial" );
|
||||
UIValueConverter<std::string> nonEmpty(
|
||||
[]( const CSS::PropertyDefinition*,
|
||||
const std::string& candidate ) -> UIValueResult<std::string> {
|
||||
return candidate.empty() ? UIValueResult<std::string>::error( 300, "empty value" )
|
||||
: UIValueResult<std::string>::success( candidate );
|
||||
},
|
||||
UIValueConverter<std::string>::converterString().fromValue );
|
||||
auto binding = bindValue( value, widget, nonEmpty, "text", Event::OnTextChanged );
|
||||
|
||||
widget->setText( "" );
|
||||
EXPECT_TRUE( value.get() == "initial" );
|
||||
EXPECT_FALSE( binding.isValid() );
|
||||
EXPECT_EQ( *binding.validationState()->code(), 300u );
|
||||
|
||||
widget->setText( "valid" );
|
||||
EXPECT_TRUE( value.get() == "valid" );
|
||||
EXPECT_TRUE( binding.isValid() );
|
||||
|
||||
value = "";
|
||||
EXPECT_TRUE( widget->getText().empty() );
|
||||
EXPECT_TRUE( binding.isValid() );
|
||||
|
||||
eeDelete( widget );
|
||||
EXPECT_TRUE( binding.isValid() );
|
||||
}
|
||||
|
||||
UTEST( UIValueBinding, convertsFormattedModelValues ) {
|
||||
UIApplication app(
|
||||
WindowSettings( 320, 240, "eepp - UIValueBinding Converter Test", WindowStyle::Default,
|
||||
WindowBackend::Default, 32 ),
|
||||
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
|
||||
auto widget = UITextInput::New();
|
||||
ObservableValue<double> amount( 10.0 );
|
||||
UIValueConverter<double> euro(
|
||||
[]( const CSS::PropertyDefinition*, const std::string& text ) -> UIValueResult<double> {
|
||||
const std::string prefix( "€" );
|
||||
double value;
|
||||
if ( text.rfind( prefix, 0 ) != 0 ||
|
||||
!String::fromString( value, text.substr( prefix.size() ) ) )
|
||||
return UIValueResult<double>::error( 400, "expected an EUR amount" );
|
||||
if ( value < 0 )
|
||||
return UIValueResult<double>::error( 401 );
|
||||
return value;
|
||||
},
|
||||
[]( const CSS::PropertyDefinition*, double value ) {
|
||||
return UIValueResult<std::string>( "€" + String::fromDouble( value ) );
|
||||
} );
|
||||
auto binding = bindValue( amount, widget, euro, "text", Event::OnTextChanged );
|
||||
|
||||
EXPECT_TRUE( widget->getText() == "€10" );
|
||||
widget->setText( "€25" );
|
||||
EXPECT_EQ( amount.get(), 25.0 );
|
||||
EXPECT_TRUE( binding.isValid() );
|
||||
|
||||
widget->setText( "USD 30" );
|
||||
EXPECT_EQ( amount.get(), 25.0 );
|
||||
EXPECT_EQ( *binding.validationState()->code(), 400u );
|
||||
|
||||
widget->setText( "€-5" );
|
||||
EXPECT_EQ( amount.get(), 25.0 );
|
||||
EXPECT_EQ( *binding.validationState()->code(), 401u );
|
||||
|
||||
amount = -5;
|
||||
EXPECT_TRUE( widget->getText() == "€-5" );
|
||||
EXPECT_TRUE( binding.isValid() );
|
||||
|
||||
eeDelete( widget );
|
||||
}
|
||||
|
||||
@@ -3,10 +3,45 @@
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/ui/uicheckbox.hpp>
|
||||
#include <eepp/ui/uiproperty.hpp>
|
||||
#include <eepp/ui/uitextinput.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::UI;
|
||||
|
||||
UTEST( UIValueValidation, supportsCodesAndOptionalDiagnostics ) {
|
||||
auto success = UIValueValidationResult::success();
|
||||
EXPECT_TRUE( static_cast<bool>( success ) );
|
||||
EXPECT_FALSE( success.code.has_value() );
|
||||
EXPECT_FALSE( success.debugMessage.has_value() );
|
||||
|
||||
auto coded = UIValueValidationResult::error( 42 );
|
||||
EXPECT_FALSE( static_cast<bool>( coded ) );
|
||||
EXPECT_TRUE( coded.code.has_value() );
|
||||
EXPECT_EQ( *coded.code, 42u );
|
||||
EXPECT_FALSE( coded.debugMessage.has_value() );
|
||||
|
||||
auto diagnosed = UIValueValidationResult::error( 43, "technical detail" );
|
||||
EXPECT_EQ( *diagnosed.code, 43u );
|
||||
EXPECT_TRUE( *diagnosed.debugMessage == "technical detail" );
|
||||
auto diagnosticOnly = UIValueValidationResult::error( std::string( "only diagnostic" ) );
|
||||
EXPECT_FALSE( diagnosticOnly.code.has_value() );
|
||||
EXPECT_TRUE( *diagnosticOnly.debugMessage == "only diagnostic" );
|
||||
}
|
||||
|
||||
UTEST( UIValueValidation, observesOnlyDistinctResultChanges ) {
|
||||
UIValueValidationState state;
|
||||
int notifications = 0;
|
||||
auto connection = state.observe( [&]( const UIValueValidationResult& ) { ++notifications; } );
|
||||
|
||||
state.set( UIValueValidationResult::error( 7 ) );
|
||||
state.set( UIValueValidationResult::error( 7 ) );
|
||||
state.set( UIValueValidationResult::error( 7, "detail" ) );
|
||||
state.clear();
|
||||
|
||||
EXPECT_EQ( notifications, 3 );
|
||||
EXPECT_TRUE( state.isValid() );
|
||||
}
|
||||
|
||||
UTEST( UIProperty, defaultConstructionOwnsUsableValue ) {
|
||||
UIProperty<int> property;
|
||||
EXPECT_EQ( property.value(), 0 );
|
||||
@@ -69,9 +104,9 @@ UTEST( UIProperty, stringConcatenationPropagatesForStandardAndEEStrings ) {
|
||||
|
||||
UTEST( UIDataBind, defaultStringConverterReadsWidgetValue ) {
|
||||
auto converter = UIDataBind<std::string>::converterDefault();
|
||||
std::string value;
|
||||
EXPECT_TRUE( converter.toValue( nullptr, value, "widget value" ) );
|
||||
EXPECT_TRUE( value == "widget value" );
|
||||
auto value = converter.toValue( nullptr, "widget value" );
|
||||
EXPECT_TRUE( value );
|
||||
EXPECT_TRUE( *value.value == "widget value" );
|
||||
}
|
||||
|
||||
UTEST( UIDataBind, lateBoundWidgetReceivesValueAndCanDieFirst ) {
|
||||
@@ -119,3 +154,98 @@ UTEST( UIDataBind, supportsMultipleWidgetsAndDisconnectsWhenBindingDiesFirst ) {
|
||||
eeDelete( firstWidget );
|
||||
eeDelete( secondWidget );
|
||||
}
|
||||
|
||||
UTEST( UIDataBind, validatesWidgetProposalsButAcceptsAuthoritativeModelValues ) {
|
||||
UIApplication app(
|
||||
WindowSettings( 320, 240, "eepp - UIDataBind Validation Test", WindowStyle::Default,
|
||||
WindowBackend::Default, 32 ),
|
||||
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
|
||||
std::string value( "initial" );
|
||||
auto firstWidget = UITextInput::New();
|
||||
auto secondWidget = UITextInput::New();
|
||||
UIValueConverter<std::string> converter(
|
||||
[]( const CSS::PropertyDefinition*,
|
||||
const std::string& candidate ) -> UIValueResult<std::string> {
|
||||
return candidate == "invalid" ? UIValueResult<std::string>::error( 100 )
|
||||
: UIValueResult<std::string>::success( candidate );
|
||||
},
|
||||
UIValueConverter<std::string>::converterString().fromValue );
|
||||
UIDataBind<std::string> binding( &value, UnorderedSet<UIWidget*>{ firstWidget, secondWidget },
|
||||
converter, "text", Event::OnTextChanged );
|
||||
|
||||
firstWidget->setText( "invalid" );
|
||||
EXPECT_TRUE( value == "initial" );
|
||||
EXPECT_TRUE( firstWidget->getText() == "invalid" );
|
||||
EXPECT_TRUE( secondWidget->getText() == "initial" );
|
||||
EXPECT_FALSE( binding.isValid() );
|
||||
EXPECT_EQ( *binding.validationState().code(), 100u );
|
||||
|
||||
auto result = binding.set( std::string( "invalid" ) );
|
||||
EXPECT_TRUE( static_cast<bool>( result ) );
|
||||
EXPECT_TRUE( value == "invalid" );
|
||||
EXPECT_TRUE( binding.isValid() );
|
||||
|
||||
EXPECT_TRUE( binding.set( std::string( "initial" ) ) );
|
||||
firstWidget->setText( "initial" );
|
||||
firstWidget->setText( "invalid" );
|
||||
eeDelete( firstWidget );
|
||||
firstWidget = nullptr;
|
||||
EXPECT_TRUE( binding.isValid() );
|
||||
|
||||
firstWidget = UITextInput::New();
|
||||
binding.bind( firstWidget );
|
||||
firstWidget->setText( "valid" );
|
||||
EXPECT_TRUE( value == "valid" );
|
||||
EXPECT_TRUE( secondWidget->getText() == "valid" );
|
||||
EXPECT_TRUE( binding.isValid() );
|
||||
|
||||
eeDelete( firstWidget );
|
||||
eeDelete( secondWidget );
|
||||
}
|
||||
|
||||
UTEST( UIDataBind, defaultNumericConversionFailurePreservesModel ) {
|
||||
UIApplication app(
|
||||
WindowSettings( 320, 240, "eepp - UIDataBind Parse Validation Test", WindowStyle::Default,
|
||||
WindowBackend::Default, 32 ),
|
||||
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
|
||||
auto widget = UITextInput::New();
|
||||
int value = 5;
|
||||
UIDataBind<int> binding( &value, widget, UIDataBind<int>::converterDefault(), "text",
|
||||
Event::OnTextChanged );
|
||||
|
||||
widget->setText( "not a number" );
|
||||
EXPECT_EQ( value, 5 );
|
||||
EXPECT_FALSE( binding.isValid() );
|
||||
EXPECT_EQ( *binding.validationState().code(),
|
||||
static_cast<Uint32>( UIValueValidationError::ConversionFailed ) );
|
||||
|
||||
eeDelete( widget );
|
||||
}
|
||||
|
||||
UTEST( UIDataBind, failedModelToWidgetConversionDoesNotApplyPartialText ) {
|
||||
UIApplication app(
|
||||
WindowSettings( 320, 240, "eepp - UIDataBind Conversion Test", WindowStyle::Default,
|
||||
WindowBackend::Default, 32 ),
|
||||
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
|
||||
auto widget = UITextInput::New();
|
||||
int value = 1;
|
||||
UIValueConverter<int> converter(
|
||||
[]( const CSS::PropertyDefinition*, const std::string& ) {
|
||||
return UIValueResult<int>::success( 1 );
|
||||
},
|
||||
[]( const CSS::PropertyDefinition*, const int& converted ) {
|
||||
return converted == 2 ? UIValueResult<std::string>::error( 200 )
|
||||
: UIValueResult<std::string>::success( "partial" );
|
||||
} );
|
||||
UIDataBind<int> binding( &value, widget, converter, "text", Event::OnTextChanged );
|
||||
EXPECT_TRUE( widget->getText() == "partial" );
|
||||
|
||||
auto result = binding.set( 2 );
|
||||
EXPECT_FALSE( static_cast<bool>( result ) );
|
||||
EXPECT_TRUE( widget->getText() == "partial" );
|
||||
EXPECT_EQ( value, 2 );
|
||||
EXPECT_FALSE( binding.set( 2 ) );
|
||||
EXPECT_FALSE( binding.isValid() );
|
||||
|
||||
eeDelete( widget );
|
||||
}
|
||||
|
||||
@@ -127,20 +127,18 @@ class UICustomOutputParserWindow : public UIWindow {
|
||||
|
||||
UIDropDownList* cpTypeddl = find<UIDropDownList>( "custom_parser_type" );
|
||||
UIDataBind<ProjectOutputParserTypes>::Converter projectOutputParserTypesConverter(
|
||||
[]( const PropertyDefinition* property, ProjectOutputParserTypes& val,
|
||||
const std::string& str ) -> bool {
|
||||
[]( const PropertyDefinition* property, const std::string& str ) {
|
||||
auto v = StyleSheetProperty( property, str ).asString();
|
||||
Uint32 idx;
|
||||
if ( String::fromString( idx, v ) && idx >= 0 && idx <= 2 ) {
|
||||
val = (ProjectOutputParserTypes)idx;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if ( String::fromString( idx, v ) && idx <= 2 )
|
||||
return UIValueResult<ProjectOutputParserTypes>::success(
|
||||
static_cast<ProjectOutputParserTypes>( idx ) );
|
||||
return UIValueResult<ProjectOutputParserTypes>::error(
|
||||
static_cast<Uint32>( UIValueValidationError::ConversionFailed ) );
|
||||
},
|
||||
[cpTypeddl]( const PropertyDefinition*, std::string& str,
|
||||
const ProjectOutputParserTypes& val ) -> bool {
|
||||
str = cpTypeddl->getListBox()->getItem( (Uint32)val )->getText();
|
||||
return true;
|
||||
[cpTypeddl]( const PropertyDefinition*, const ProjectOutputParserTypes& val ) {
|
||||
return UIValueResult<std::string>::success(
|
||||
cpTypeddl->getListBox()->getItem( static_cast<Uint32>( val ) )->getText() );
|
||||
} );
|
||||
|
||||
mDataBindHolder += UIDataBind<ProjectOutputParserTypes>::New(
|
||||
|
||||
Reference in New Issue
Block a user