diff --git a/source/MRViewer/MRUITestEngineControl.cpp b/source/MRViewer/MRUITestEngineControl.cpp index 7b9bb05c3ca2..a272eadff42a 100644 --- a/source/MRViewer/MRUITestEngineControl.cpp +++ b/source/MRViewer/MRUITestEngineControl.cpp @@ -157,7 +157,7 @@ Expected> listAllEntries( const std::vector pressButton( const std::vector& path ) +Expected pressButton( const std::vector& path ) { if ( path.empty() ) return unexpected( "pressButton: Empty path not allowed here." ); @@ -177,7 +177,7 @@ Expected pressButton( const std::vector& path ) const std::string_view disabledReason = ( *buttonEx )->disabledReason; if ( !disabledReason.empty() ) - return unexpected( fmt::format( "pressButton {}: {}", pathToString( path ), composeStatus( disabledReason ) ) ); + return composeStatus( disabledReason ); ( *buttonEx )->simulateClick = true; @@ -276,7 +276,7 @@ template Expected> readValue( const std::vector -Expected writeValue( const std::vector& path, T value ) +Expected writeValue( const std::vector& path, T value ) { if ( path.empty() ) return unexpected( "writeValue: Empty path not allowed here." ); @@ -297,9 +297,9 @@ Expected writeValue( const std::vector& path, T value ) const std::string_view disabledReason = entry.disabledReason; if ( !disabledReason.empty() ) - return unexpected( fmt::format( "writeValue {}: {}", pathToString( path ), composeStatus( disabledReason ) ) ); + return composeStatus( disabledReason ); - auto writeValueOfCorrectType = [&entry, &path]( auto fixedValue ) -> Expected + auto writeValueOfCorrectType = [&entry, &path]( auto fixedValue ) -> Expected { using U = decltype( fixedValue ); auto &target = std::get>( entry.value ); @@ -347,19 +347,19 @@ Expected writeValue( const std::vector& path, T value ) else if constexpr ( std::is_same_v ) { return std::visit( MR::overloaded{ - [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); }, - [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( value ); }, - [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathToString( path ) ) ); }, - [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathToString( path ) ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( value ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathToString( path ) ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathToString( path ) ) ); }, }, entry.value ); } else if constexpr ( std::is_same_v ) { return std::visit( MR::overloaded{ - [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); }, - [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( double( value ) ); }, - [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( value ); }, - [&]( const TestEngine::ValueEntry::Value& ) -> Expected + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( double( value ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( value ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { if ( value < 0 ) return unexpected( fmt::format( "writeValue: `{}` is unsigned, but received a negative number.", pathToString( path ) ) ); @@ -370,10 +370,10 @@ Expected writeValue( const std::vector& path, T value ) else if constexpr ( std::is_same_v ) { return std::visit( MR::overloaded{ - [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); }, - [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( double( value ) ); }, - [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( value ); }, - [&]( const TestEngine::ValueEntry::Value& ) -> Expected + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( double( value ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( value ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { if ( value > std::uint64_t( std::numeric_limits::max() ) ) return unexpected( fmt::format( "writeValue: `{}` is signed, but received an unsigned integer large enough to not be representable as `int64_t`.", pathToString( path ) ) ); @@ -383,9 +383,9 @@ Expected writeValue( const std::vector& path, T value ) } } -template Expected writeValue( const std::vector& path, std::int64_t value ); -template Expected writeValue( const std::vector& path, std::uint64_t value ); -template Expected writeValue( const std::vector& path, double value ); -template Expected writeValue( const std::vector& path, std::string value ); +template Expected writeValue( const std::vector& path, std::int64_t value ); +template Expected writeValue( const std::vector& path, std::uint64_t value ); +template Expected writeValue( const std::vector& path, double value ); +template Expected writeValue( const std::vector& path, std::string value ); } // namespace MR::UI::TestEngine::Control diff --git a/source/MRViewer/MRUITestEngineControl.h b/source/MRViewer/MRUITestEngineControl.h index 8cf2b752104e..4c6a3e74b8f6 100644 --- a/source/MRViewer/MRUITestEngineControl.h +++ b/source/MRViewer/MRUITestEngineControl.h @@ -68,8 +68,11 @@ using PathedEntry = std::pair, TypedEntry>; // and their descendants appear on subsequent rows with `path` extending theirs. [[nodiscard]] MRVIEWER_API Expected> listAllEntries( const std::vector& rootPath ); -// Presses the button at this path, or returns an error. -MRVIEWER_API Expected pressButton( const std::vector& path ); +// Presses the button at this path. +// Returns empty string on success (click simulated). If the button was drawn disabled, the press is a silent +// no-op and the return is a non-empty status (`"disabled"` / `"disabled: "`) matching `composeStatus()`. +// `unexpected` is returned only for hard errors (path not found, entry is not a button). +MRVIEWER_API Expected pressButton( const std::vector& path ); // Read/write values: (drags, sliders, etc) @@ -101,14 +104,17 @@ extern template MRVIEWER_API Expected> readValue( const std extern template MRVIEWER_API Expected> readValue( const std::vector& path ); extern template MRVIEWER_API Expected> readValue( const std::vector& path ); -// Modifies the value at the `path`, or returns an error if the path, type or value are wrong. +// Modifies the value at the `path`. +// Returns empty string on success (write simulated). If the widget was drawn disabled, the write is a silent +// no-op and the return is a non-empty status (`"disabled"` / `"disabled: "`) matching `composeStatus()`. +// `unexpected` is returned only for hard errors (path not found, wrong type, out-of-range / not-in-allowedValues). template -MRVIEWER_API Expected writeValue( const std::vector& path, T value ); +MRVIEWER_API Expected writeValue( const std::vector& path, T value ); -extern template MRVIEWER_API Expected writeValue( const std::vector& path, std::int64_t value ); -extern template MRVIEWER_API Expected writeValue( const std::vector& path, std::uint64_t value ); -extern template MRVIEWER_API Expected writeValue( const std::vector& path, double value ); -extern template MRVIEWER_API Expected writeValue( const std::vector& path, std::string value ); +extern template MRVIEWER_API Expected writeValue( const std::vector& path, std::int64_t value ); +extern template MRVIEWER_API Expected writeValue( const std::vector& path, std::uint64_t value ); +extern template MRVIEWER_API Expected writeValue( const std::vector& path, double value ); +extern template MRVIEWER_API Expected writeValue( const std::vector& path, std::string value ); } // namespace MR::UI::TestEngine::Control diff --git a/source/MRViewer/MRViewerMcp.cpp b/source/MRViewer/MRViewerMcp.cpp index 6410faa49d14..87e092953baa 100644 --- a/source/MRViewer/MRViewerMcp.cpp +++ b/source/MRViewer/MRViewerMcp.cpp @@ -2,6 +2,7 @@ #include "MRMcp/MRMcp.h" #include "MRMesh/MROnInit.h" +#include "MRPch/MRFmt.h" #include "MRViewer/MRCommandLoop.h" #include "MRViewer/MRUITestEngineControl.h" @@ -75,11 +76,15 @@ static nlohmann::json mcpToolListAllUiEntries( const nlohmann::json& args ) static nlohmann::json mcpToolPressButton( const nlohmann::json& args ) { + const auto path = args.at( "path" ).get>(); MR::CommandLoop::runCommandFromGUIThread( [&] { - auto ex = UI::TestEngine::Control::pressButton( args.at( "path" ).get>() ); + auto ex = UI::TestEngine::Control::pressButton( path ); if ( !ex ) throw std::runtime_error( ex.error() ); + // Non-empty = disabled status; surface to MCP as an error (empty = OK, click simulated). + if ( !ex->empty() ) + throw std::runtime_error( fmt::format( "pressButton {}: {}", UI::TestEngine::Control::pathToString( path ), *ex ) ); } ); skipFramesAfterInput(); @@ -117,11 +122,15 @@ static nlohmann::json mcpToolReadValue( const nlohmann::json& args ) template static nlohmann::json mcpToolWriteValue( const nlohmann::json& args ) { + const auto path = args.at( "path" ).get>(); MR::CommandLoop::runCommandFromGUIThread( [&] { - auto ex = UI::TestEngine::Control::writeValue( args.at( "path" ).get>(), T( args.at( "value" ) ) ); + auto ex = UI::TestEngine::Control::writeValue( path, T( args.at( "value" ) ) ); if ( !ex ) throw std::runtime_error( ex.error() ); + // Non-empty = disabled status; surface to MCP as an error (empty = OK, write simulated). + if ( !ex->empty() ) + throw std::runtime_error( fmt::format( "writeValue {}: {}", UI::TestEngine::Control::pathToString( path ), *ex ) ); } ); skipFramesAfterInput(); diff --git a/source/mrviewerpy/MRPythonUiInteraction.cpp b/source/mrviewerpy/MRPythonUiInteraction.cpp index e06911f2a4c9..a28fb979fba4 100644 --- a/source/mrviewerpy/MRPythonUiInteraction.cpp +++ b/source/mrviewerpy/MRPythonUiInteraction.cpp @@ -75,7 +75,10 @@ MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiPressButton, MR::CommandLoop::runCommandFromGUIThread( [&] { spdlog::info( "pressButton {}: frame {}", MR::UI::TestEngine::Control::pathToString( path ), MR::getViewerInstance().getTotalFrames() ); - MR::expectedValueOrThrow( MR::UI::TestEngine::Control::pressButton( path ) ); + // Empty status = OK (click simulated); non-empty = disabled (silent no-op — pre-#5961 test contract). + auto status = MR::expectedValueOrThrow( MR::UI::TestEngine::Control::pressButton( path ) ); + if ( !status.empty() ) + spdlog::warn( "pressButton {}: {} (silent no-op)", MR::UI::TestEngine::Control::pathToString( path ), status ); } ); for ( int i = 0; i < MR::getViewerInstance().forceRedrawMinimumIncrementAfterEvents; ++i ) MR::CommandLoop::runCommandFromGUIThread( [] {} ); // Wait a few frames. @@ -117,7 +120,10 @@ namespace { MR::CommandLoop::runCommandFromGUIThread( [&] { - MR::expectedValueOrThrow( Control::writeValue( path, std::move( value ) ) ); + // Empty status = OK (write simulated); non-empty = disabled (silent no-op — pre-#5961 test contract). + auto status = MR::expectedValueOrThrow( Control::writeValue( path, std::move( value ) ) ); + if ( !status.empty() ) + spdlog::warn( "writeValue {}: {} (silent no-op)", Control::pathToString( path ), status ); } ); } }