Files
oak-editor/tests/gtest/widget_keyframe_multicam_test.cpp
Mike-Solar 82162580ef test(app): add gtest coverage for previously untested app modules
Add 312 test cases across 14 new files under tests/gtest, covering:

- app/common utility headers (tooltypes, projecttypes, nodedatatypes,
  trackreferencehandle, undowrapper, configwrapper, subtitleapp, ...)
- all 8 standardcombos widgets
- slider base classes (NumericSliderBase, SliderLadder)
- timeline/track handles, marker handle/painting, drag button,
  elapsed counter
- timebased views, selection manager, time target, resizable timeline
  scrollbar
- viewer sizer, display buffer, playback timer, text editor
- timeline common helpers, TimelineAndTrackView, TrackView(-item/-splitter)
- project explorer views, navigation and undo commands
- node view items/edges/connectors/minimap/toolbar/context
- node param view sub-widgets
- about/export/sequence/footage-properties dialogs
- scope widgets (histogram/vectorscope/waveform), managed display
- main menu, main window undo commands, viewer/footage panels,
  engine event bridge
- keyframe view handle/input connection, multicam display,
  node combo box, audio monitor

Includes regression tests for the TimelineAndTrackView teardown and
TimeScaledObject inverted-limits fixes. Full suite: 2341 tests, 0 failures
(64 skips are pre-existing GL-dependent cases under the offscreen QPA).
2026-08-05 14:53:26 +08:00

661 lines
20 KiB
C++

#include <gtest/gtest.h>
#include <cstring>
#include <memory>
#include <QApplication>
#include <QDialogButtonBox>
#include <QEvent>
#include <QPushButton>
#include <QSignalSpy>
#include <QTreeWidget>
#include <olive/core/render/audioparams.h>
#include <olive/core/render/samplebuffer.h>
#include "core.h"
#include "dialog/configbase/configdialogbasetab.h"
#include "dialog/otioproperties/otiopropertiesdialog.h"
#include "node/color/colormanager/colormanager.h"
#include "node/factory.h"
#include "node/input/multicam/multicamnode.h"
#include "node/keyframe.h"
#include "node/math/math/math.h"
#include "node/nodeundo.h"
#include "node/project.h"
#include "node/project/sequence/sequence.h"
#include "oakengine/node.h"
#include "render/diskmanager.h"
#include "widget/audiomonitor/audiomonitor.h"
#include "widget/keyframeview/keyframehandle.h"
#include "widget/keyframeview/keyframeview.h"
#include "widget/keyframeview/keyframeviewinputconnection.h"
#include "widget/multicam/multicamdisplay.h"
#include "widget/nodecombobox/nodecombobox.h"
using namespace olive;
namespace
{
// Keyframe views and the facade event subscriptions need the application
// singletons; create them once and leak them (same pattern as the other
// widget suites)
void ensure_app_singletons()
{
if (!olive::Core::instance()) {
new olive::Core(); // intentionally leaked
}
if (!olive::DiskManager::instance()) {
olive::DiskManager::create_instance();
}
}
NodeKeyframe *insert_keyframe(Node *node, const QString &input,
const Rational &time, const QVariant &value,
int track = 0)
{
auto *key =
new NodeKeyframe(time, value, NodeKeyframe::k_linear, track, -1, input);
NodeParamInsertKeyframeCommand(node, key).redo_now();
return key;
}
oak::KeyframeTrackRef track_ref(Node *n, const QString &input, int track)
{
return oak::KeyframeTrackRef(
oak::Input(reinterpret_cast<OakEngineNode *>(n), input), track);
}
OakEngineKeyframe *handle_of(NodeKeyframe *key)
{
return reinterpret_cast<OakEngineKeyframe *>(key);
}
// Minimal concrete tab: the base class only provides a default validate()
class RecordingTab : public olive::ConfigDialogBaseTab {
public:
virtual void accept(void *parent) override
{
accepted_with = parent;
}
void *accepted_with = nullptr;
};
} // namespace
//
// keyframehandle.h: facade accessors over OakEngineKeyframe identity handles
//
class KeyframeHandleTest : public ::testing::Test {
protected:
void SetUp() override
{
ColorManager::set_up_default_config();
ensure_app_singletons();
project_ = std::make_unique<Project>();
project_->initialize();
node_ = new MathNode();
node_->setParent(project_.get());
}
std::unique_ptr<Project> project_;
MathNode *node_ = nullptr;
};
TEST_F(KeyframeHandleTest, AccessorsReadEngineState)
{
NodeKeyframe *key = insert_keyframe(node_, MathNode::k_param_a_in,
Rational(1, 2), 2.5);
const OakEngineKeyframe *handle = handle_of(key);
EXPECT_EQ(key_node(handle), reinterpret_cast<OakEngineNode *>(node_));
EXPECT_EQ(key_time(handle), Rational(1, 2));
EXPECT_EQ(key_easing(handle), 0); // facade order: 0 = linear
EXPECT_EQ(key_input_id(handle), MathNode::k_param_a_in);
EXPECT_EQ(key_track(handle), 0);
EXPECT_EQ(key_element(handle), -1);
}
TEST_F(KeyframeHandleTest, ValueAsDoubleConvertsNumericTypes)
{
NodeKeyframe *float_key = insert_keyframe(node_, MathNode::k_param_a_in,
Rational(0), 2.5);
const oak_node_value float_value = key_value(handle_of(float_key));
EXPECT_EQ(float_value.type, OAK_NODE_VALUE_FLOAT);
EXPECT_DOUBLE_EQ(key_value_as_double(handle_of(float_key)), 2.5);
// The base-class enabled input carries boolean keyframes
NodeKeyframe *bool_key = insert_keyframe(node_, Node::k_enabled_input,
Rational(0), true);
const oak_node_value bool_value = key_value(handle_of(bool_key));
EXPECT_EQ(bool_value.type, OAK_NODE_VALUE_BOOL);
EXPECT_DOUBLE_EQ(key_value_as_double(handle_of(bool_key)), 1.0);
NodeKeyframe *false_key = insert_keyframe(node_, Node::k_enabled_input,
Rational(1), false);
EXPECT_DOUBLE_EQ(key_value_as_double(handle_of(false_key)), 0.0);
}
TEST_F(KeyframeHandleTest, LiveValueSetUpdatesFacadeReads)
{
NodeKeyframe *key = insert_keyframe(node_, MathNode::k_param_a_in,
Rational(0), 1.0);
oak_node_value v;
std::memset(&v, 0, sizeof(v));
v.type = OAK_NODE_VALUE_FLOAT;
v.f[0] = 7.25;
key_set_value_live(handle_of(key), v);
EXPECT_DOUBLE_EQ(key_value_as_double(handle_of(key)), 7.25);
EXPECT_DOUBLE_EQ(key->value().toDouble(), 7.25);
}
TEST_F(KeyframeHandleTest, LiveTimeSetMovesKeyframe)
{
NodeKeyframe *key = insert_keyframe(node_, MathNode::k_param_a_in,
Rational(0), 0.0);
key_set_time_live(handle_of(key), Rational(2));
EXPECT_EQ(key_time(handle_of(key)), Rational(2));
EXPECT_EQ(key->time(), Rational(2));
// The selection ADL wrappers route through the same accessors
EXPECT_EQ(selection_time(handle_of(key)), Rational(2));
selection_set_time(handle_of(key), Rational(4));
EXPECT_EQ(key->time(), Rational(4));
}
TEST_F(KeyframeHandleTest, BezierPointRoundTrip)
{
NodeKeyframe *key = insert_keyframe(node_, MathNode::k_param_a_in,
Rational(0), 0.0);
key->set_type(NodeKeyframe::k_bezier);
ASSERT_EQ(key_easing(handle_of(key)), 1); // facade order: 1 = bezier
key_set_bezier_point_live(handle_of(key), 0, QPointF(0.25, 0.75));
key_set_bezier_point_live(handle_of(key), 1, QPointF(-0.5, 1.5));
EXPECT_EQ(key_bezier_point(handle_of(key), 0), QPointF(0.25, 0.75));
EXPECT_EQ(key_bezier_point(handle_of(key), 1), QPointF(-0.5, 1.5));
// For a bezier keyframe the "valid" points are the actual points
EXPECT_EQ(key_valid_bezier_point(handle_of(key), 0), QPointF(0.25, 0.75));
EXPECT_EQ(key_valid_bezier_point(handle_of(key), 1), QPointF(-0.5, 1.5));
}
TEST_F(KeyframeHandleTest, HasSiblingAtTimeExcludesSelf)
{
// The sibling lookup goes through Node::get_keyframe_at_time_on_track(),
// which only searches tracks that are not on the standard value, so
// keyframing must be enabled on the input first
node_->set_input_is_keyframing(MathNode::k_param_a_in, true);
NodeKeyframe *key_a = insert_keyframe(node_, MathNode::k_param_a_in,
Rational(0), 0.0);
NodeKeyframe *key_b = insert_keyframe(node_, MathNode::k_param_a_in,
Rational(1), 1.0);
// Another keyframe sits at t=1
EXPECT_TRUE(key_has_sibling_at_time(handle_of(key_a), Rational(1)));
// t=0 resolves to key_a itself, which does not count as a sibling
EXPECT_FALSE(key_has_sibling_at_time(handle_of(key_a), Rational(0)));
// Nothing at t=5
EXPECT_FALSE(key_has_sibling_at_time(handle_of(key_a), Rational(5)));
EXPECT_TRUE(selection_has_sibling_at_time(handle_of(key_b), Rational(0)));
EXPECT_EQ(selection_time_target_parent(handle_of(key_a)),
reinterpret_cast<OakEngineNode *>(node_));
}
//
// keyframeviewinputconnection.h: per-track connection between engine keyframe
// events and a KeyframeView
//
class KeyframeConnectionTest : public ::testing::Test {
protected:
void SetUp() override
{
ColorManager::set_up_default_config();
ensure_app_singletons();
project_ = std::make_unique<Project>();
project_->initialize();
node_ = new MathNode();
node_->setParent(project_.get());
}
oak::KeyframeTrackRef param_a_ref() const
{
return track_ref(node_, MathNode::k_param_a_in, 0);
}
std::unique_ptr<Project> project_;
MathNode *node_ = nullptr;
};
TEST_F(KeyframeConnectionTest, DefaultsAndAccessors)
{
KeyframeView view;
const oak::KeyframeTrackRef ref = param_a_ref();
KeyframeViewInputConnection connection(ref, &view);
EXPECT_EQ(connection.get_keyframe_y(), 0);
EXPECT_EQ(connection.get_brush().color(), QColor(Qt::white));
EXPECT_EQ(connection.get_reference(), ref);
EXPECT_TRUE(connection.get_keyframes().isEmpty());
}
TEST_F(KeyframeConnectionTest, SetKeyframeYEmitsOnlyOnChange)
{
KeyframeView view;
KeyframeViewInputConnection connection(param_a_ref(), &view);
QSignalSpy spy(&connection, &KeyframeViewInputConnection::require_update);
connection.set_keyframe_y(10);
EXPECT_EQ(connection.get_keyframe_y(), 10);
EXPECT_EQ(spy.count(), 1);
// Same value is a no-op
connection.set_keyframe_y(10);
EXPECT_EQ(spy.count(), 1);
connection.set_keyframe_y(-3);
EXPECT_EQ(connection.get_keyframe_y(), -3);
EXPECT_EQ(spy.count(), 2);
}
TEST_F(KeyframeConnectionTest, SetBrushEmitsOnlyOnChange)
{
KeyframeView view;
KeyframeViewInputConnection connection(param_a_ref(), &view);
QSignalSpy spy(&connection, &KeyframeViewInputConnection::require_update);
// Same as the default brush: no emission
connection.set_brush(QBrush(Qt::white));
EXPECT_EQ(spy.count(), 0);
connection.set_brush(QBrush(Qt::red));
EXPECT_EQ(connection.get_brush().color(), QColor(Qt::red));
EXPECT_EQ(spy.count(), 1);
}
TEST_F(KeyframeConnectionTest, SetYBehaviorEmitsOnlyOnChange)
{
KeyframeView view;
KeyframeViewInputConnection connection(param_a_ref(), &view);
QSignalSpy spy(&connection, &KeyframeViewInputConnection::require_update);
// Default behavior is k_single_row
connection.set_y_behavior(KeyframeViewInputConnection::k_single_row);
EXPECT_EQ(spy.count(), 0);
connection.set_y_behavior(KeyframeViewInputConnection::k_value_is_height);
EXPECT_EQ(spy.count(), 1);
connection.set_y_behavior(KeyframeViewInputConnection::k_value_is_height);
EXPECT_EQ(spy.count(), 1);
}
TEST_F(KeyframeConnectionTest, MatchingKeyframeInsertionEmitsRequireUpdate)
{
KeyframeView view;
KeyframeViewInputConnection connection(param_a_ref(), &view);
QSignalSpy spy(&connection, &KeyframeViewInputConnection::require_update);
insert_keyframe(node_, MathNode::k_param_a_in, Rational(0), 0.0);
EXPECT_EQ(spy.count(), 1);
ASSERT_EQ(connection.get_keyframes().size(), 1);
// A keyframe on a different input of the same node does not match
insert_keyframe(node_, MathNode::k_param_b_in, Rational(0), 0.0);
EXPECT_EQ(spy.count(), 1);
EXPECT_EQ(connection.get_keyframes().size(), 1);
}
TEST_F(KeyframeConnectionTest, LiveValueAndTimeChangesEmitRequireUpdate)
{
NodeKeyframe *key = insert_keyframe(node_, MathNode::k_param_a_in,
Rational(0), 0.0);
KeyframeView view;
KeyframeViewInputConnection connection(param_a_ref(), &view);
QSignalSpy spy(&connection, &KeyframeViewInputConnection::require_update);
oak_node_value v;
std::memset(&v, 0, sizeof(v));
v.type = OAK_NODE_VALUE_FLOAT;
v.f[0] = 3.0;
key_set_value_live(handle_of(key), v);
EXPECT_EQ(spy.count(), 1);
key_set_time_live(handle_of(key), Rational(3));
EXPECT_EQ(spy.count(), 2);
}
TEST_F(KeyframeConnectionTest, TypeChangeEmitsTypeChangedAndRequireUpdate)
{
// Node::invalidate_from_keyframe_type_changed() returns early when the
// track holds a single keyframe (interpolation is a no-op), so the type
// change signal only reaches the connection with two or more keyframes
NodeKeyframe *key = insert_keyframe(node_, MathNode::k_param_a_in,
Rational(0), 0.0);
insert_keyframe(node_, MathNode::k_param_a_in, Rational(1), 1.0);
KeyframeView view;
KeyframeViewInputConnection connection(param_a_ref(), &view);
QSignalSpy update_spy(&connection,
&KeyframeViewInputConnection::require_update);
QSignalSpy type_spy(&connection,
&KeyframeViewInputConnection::type_changed);
key->set_type(NodeKeyframe::k_hold);
EXPECT_EQ(update_spy.count(), 1);
EXPECT_EQ(type_spy.count(), 1);
// Setting the same type again emits nothing
key->set_type(NodeKeyframe::k_hold);
EXPECT_EQ(update_spy.count(), 1);
EXPECT_EQ(type_spy.count(), 1);
}
TEST_F(KeyframeConnectionTest, RemovalEmitsRequireUpdate)
{
NodeKeyframe *key = insert_keyframe(node_, MathNode::k_param_a_in,
Rational(0), 0.0);
KeyframeView view;
KeyframeViewInputConnection connection(param_a_ref(), &view);
QSignalSpy spy(&connection, &KeyframeViewInputConnection::require_update);
NodeParamRemoveKeyframeCommand(key).redo_now();
EXPECT_EQ(spy.count(), 1);
EXPECT_TRUE(connection.get_keyframes().isEmpty());
}
//
// multicamdisplay.h: the shader/paint paths need a GL renderer, but the
// widget itself constructs offscreen and its node contract is facade-readable
//
TEST(WidgetMulticamDisplay, ConstructsWithoutNode)
{
ensure_app_singletons();
MulticamDisplay display;
EXPECT_FALSE(display.isVisible());
// Accepts (and clears) a null node without touching the paint path
display.set_multicam_node(nullptr);
}
TEST(WidgetMulticamDisplay, MulticamNodeStateMatchesPaintContract)
{
ensure_app_singletons();
MulticamDisplay display;
MultiCamNode node;
node.input_array_resize(MultiCamNode::k_sources_input, 3);
node.set_standard_value(MultiCamNode::k_current_input, 1);
auto *handle = reinterpret_cast<OakEngineNode *>(&node);
display.set_multicam_node(handle);
// on_paint() lays the highlight rect out from exactly these facade reads
EXPECT_EQ(oakengine_multicam_get_source_count(handle), 3);
EXPECT_EQ(oakengine_multicam_get_current_source(handle), 1);
int rows = 0, cols = 0;
oakengine_multicam_get_rows_and_columns(3, &rows, &cols);
EXPECT_EQ(rows, 2);
EXPECT_EQ(cols, 2);
int row = -1, col = -1;
oakengine_multicam_index_to_row_cols(1, rows, cols, &row, &col);
EXPECT_EQ(row, 0);
EXPECT_EQ(col, 1);
display.set_multicam_node(nullptr);
}
//
// nodecombobox.h: the popup path is modal; the retranslation and factory-name
// paths are not (selection round-trips are covered in widget_combos_test.cpp)
//
TEST(WidgetNodeComboBox, LanguageChangeRetranslatesSelection)
{
NodeFactory::initialize();
{
NodeComboBox combo;
const QString id = QStringLiteral("org.olivevideoeditor.Olive.math");
combo.set_node(id);
ASSERT_EQ(combo.count(), 1);
const QString before = combo.itemText(0);
EXPECT_EQ(before, NodeFactory::get_name_from_id(id));
ASSERT_FALSE(before.isEmpty());
QEvent lang(QEvent::LanguageChange);
QApplication::sendEvent(&combo, &lang);
// The selection survives and the label is re-fetched from the factory
EXPECT_EQ(combo.get_selected_node(), id);
ASSERT_EQ(combo.count(), 1);
EXPECT_EQ(combo.itemText(0), before);
}
NodeFactory::destroy();
}
TEST(WidgetNodeComboBox, UnknownIdProducesEmptyText)
{
NodeFactory::initialize();
{
NodeComboBox combo;
combo.set_node(QStringLiteral("org.example.bogus"));
// The id is stored even though the factory has no name for it
EXPECT_EQ(combo.get_selected_node(), QStringLiteral("org.example.bogus"));
ASSERT_EQ(combo.count(), 1);
EXPECT_TRUE(combo.itemText(0).isEmpty());
}
NodeFactory::destroy();
}
//
// audiomonitor.h: painting needs a GL context; the playback state machine,
// parameter handling and the static instance broadcasters do not
//
TEST(WidgetAudioMonitor, ConstructionDefaults)
{
AudioMonitor monitor;
EXPECT_FALSE(monitor.is_playing());
// The ctor sizes the minimum width to the font height
EXPECT_EQ(monitor.minimumWidth(), monitor.fontMetrics().height());
}
TEST(WidgetAudioMonitor, StartWaveformWithNullCacheStaysStopped)
{
AudioMonitor monitor;
// A null cache reports sample rate 0, so the start request is rejected
monitor.start_waveform(nullptr, Rational(0), 1);
EXPECT_FALSE(monitor.is_playing());
monitor.stop();
EXPECT_FALSE(monitor.is_playing());
}
TEST(WidgetAudioMonitor, PushSampleBufferWithoutParamsIsIgnored)
{
AudioMonitor monitor;
// No params set -> channel_count() is 0 -> early return
monitor.push_sample_buffer(SampleBuffer());
EXPECT_FALSE(monitor.is_playing());
}
TEST(WidgetAudioMonitor, PushSampleBufferWithParamsDoesNotStartPlayback)
{
const olive::core::AudioParams params(48000,
olive::core::k_channel_layout_stereo,
olive::core::SampleFormat::f32_p);
AudioMonitor monitor;
monitor.set_params(params);
SampleBuffer buffer(params, size_t(64));
ASSERT_TRUE(buffer.is_allocated());
float *left = buffer.data(0);
ASSERT_NE(left, nullptr);
for (int i = 0; i < 64; i++) {
left[i] = 0.5f;
}
// Level analysis runs, but sample playback is not waveform playback
monitor.push_sample_buffer(buffer);
EXPECT_FALSE(monitor.is_playing());
monitor.stop();
EXPECT_FALSE(monitor.is_playing());
}
TEST(WidgetAudioMonitor, StaticBroadcastsReachAllInstances)
{
const olive::core::AudioParams params(48000,
olive::core::k_channel_layout_stereo,
olive::core::SampleFormat::f32_p);
AudioMonitor first;
AudioMonitor second;
first.set_params(params);
second.set_params(params);
// A null waveform is rejected on every registered instance
AudioMonitor::start_waveform_on_all(nullptr, Rational(0), 1);
EXPECT_FALSE(first.is_playing());
EXPECT_FALSE(second.is_playing());
SampleBuffer buffer(params, size_t(32));
AudioMonitor::push_sample_buffer_on_all(buffer);
EXPECT_FALSE(first.is_playing());
EXPECT_FALSE(second.is_playing());
AudioMonitor::stop_on_all();
EXPECT_FALSE(first.is_playing());
EXPECT_FALSE(second.is_playing());
}
//
// configdialogbasetab.h
//
TEST(DialogConfigBaseTab, DefaultValidateAccepts)
{
RecordingTab tab;
EXPECT_TRUE(tab.validate());
}
TEST(DialogConfigBaseTab, AcceptReceivesParentPointer)
{
RecordingTab tab;
int parent_token = 0;
tab.accept(&parent_token);
EXPECT_EQ(tab.accepted_with, static_cast<void *>(&parent_token));
}
//
// otiopropertiesdialog.h: the per-sequence Settings button opens a modal
// SequenceDialog (not exercised); construction, listing and OK/Cancel are not
// modal
//
TEST(DialogOTIOProperties, ListsSequencesWithIndexedSettingsButtons)
{
ColorManager::set_up_default_config();
Project project;
project.initialize();
auto *seq_a = new Sequence();
seq_a->setParent(&project);
seq_a->set_label(QStringLiteral("Alpha"));
auto *seq_b = new Sequence();
seq_b->setParent(&project);
seq_b->set_label(QStringLiteral("Beta"));
const QList<OakEngineSequence *> sequences = {
reinterpret_cast<OakEngineSequence *>(seq_a),
reinterpret_cast<OakEngineSequence *>(seq_b),
};
OTIOPropertiesDialog dialog(
sequences, reinterpret_cast<OakEngineProject *>(&project));
EXPECT_EQ(dialog.windowTitle(), QStringLiteral("Load OpenTimelineIO Project"));
auto *table = dialog.findChild<QTreeWidget *>();
ASSERT_NE(table, nullptr);
EXPECT_EQ(table->columnCount(), 2);
EXPECT_EQ(table->headerItem()->text(0), QStringLiteral("Sequence"));
EXPECT_EQ(table->headerItem()->text(1), QStringLiteral("Actions"));
EXPECT_FALSE(table->rootIsDecorated());
ASSERT_EQ(table->topLevelItemCount(), 2);
EXPECT_EQ(table->topLevelItem(0)->text(0), QStringLiteral("Alpha"));
EXPECT_EQ(table->topLevelItem(1)->text(0), QStringLiteral("Beta"));
// Each row carries a Settings button whose "index" property is its row
for (int i = 0; i < 2; i++) {
QWidget *actions = table->itemWidget(table->topLevelItem(i), 1);
ASSERT_NE(actions, nullptr) << "row " << i;
auto *button = actions->findChild<QPushButton *>();
ASSERT_NE(button, nullptr) << "row " << i;
EXPECT_EQ(button->text(), QStringLiteral("Settings"));
EXPECT_EQ(button->property("index").toInt(), i);
}
}
TEST(DialogOTIOProperties, EmptyListAndButtonBoxResults)
{
ColorManager::set_up_default_config();
Project project;
project.initialize();
{
OTIOPropertiesDialog dialog(
{}, reinterpret_cast<OakEngineProject *>(&project));
auto *table = dialog.findChild<QTreeWidget *>();
ASSERT_NE(table, nullptr);
EXPECT_EQ(table->topLevelItemCount(), 0);
auto *buttons = dialog.findChild<QDialogButtonBox *>();
ASSERT_NE(buttons, nullptr);
QPushButton *ok = buttons->button(QDialogButtonBox::Ok);
ASSERT_NE(ok, nullptr);
ok->click();
EXPECT_EQ(dialog.result(), QDialog::Accepted);
}
{
OTIOPropertiesDialog dialog(
{}, reinterpret_cast<OakEngineProject *>(&project));
auto *buttons = dialog.findChild<QDialogButtonBox *>();
ASSERT_NE(buttons, nullptr);
QPushButton *cancel = buttons->button(QDialogButtonBox::Cancel);
ASSERT_NE(cancel, nullptr);
cancel->click();
EXPECT_EQ(dialog.result(), QDialog::Rejected);
}
}