tests: coverage for dialogs (56 cases)

- editing: speedduration, keyframeproperties, markerproperties, sequence
  presets/parameters, footage properties/relink, project properties
- misc: about, action search, autorecovery scan, config base, disk cache,
  text, progress, render cancel, task, color, key sequence editor,
  remaining preferences tabs
- export: format combo box, audio/video/subtitles tabs, H.264 sections,
  advanced video and save-preset dialogs
This commit is contained in:
2026-07-17 13:20:51 +08:00
parent 1475525eb2
commit 26d158146a
4 changed files with 1765 additions and 0 deletions
+3
View File
@@ -125,6 +125,9 @@ add_executable(olive-gtest
codec_ffmpegencoder_test.cpp codec_ffmpegencoder_test.cpp
ui_icons_test.cpp ui_icons_test.cpp
ui_style_test.cpp ui_style_test.cpp
dialog_editing_test.cpp
dialog_misc_test.cpp
dialog_export_test.cpp
) )
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test)
+834
View File
@@ -0,0 +1,834 @@
#include <gtest/gtest.h>
#include <memory>
#include <QBuffer>
#include <QCheckBox>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QGroupBox>
#include <QLineEdit>
#include <QStandardPaths>
#include <QTreeWidget>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include "core.h"
#include "dialog/footageproperties/footageproperties.h"
#include "dialog/footagerelink/footagerelinkdialog.h"
#include "dialog/keyframeproperties/keyframeproperties.h"
#include "dialog/markerproperties/markerpropertiesdialog.h"
#include "dialog/projectproperties/projectproperties.h"
#include "dialog/sequence/sequence.h"
#include "dialog/sequence/sequencedialogparametertab.h"
#include "dialog/sequence/sequencedialogpresettab.h"
#include "dialog/sequence/sequencepreset.h"
#include "dialog/speedduration/speeddurationdialog.h"
#include "node/block/clip/clip.h"
#include "node/color/colormanager/colormanager.h"
#include "node/math/math/math.h"
#include "node/output/track/track.h"
#include "node/project.h"
#include "node/project/footage/footage.h"
#include "node/project/sequence/sequence.h"
#include "render/diskmanager.h"
#include "timeline/timelinemarker.h"
#include "widget/colorlabelmenu/colorcodingcombobox.h"
#include "widget/slider/floatslider.h"
#include "widget/slider/rationalslider.h"
namespace
{
// Several of these dialogs push undo commands to the global undo stack on
// accept(), which requires the Core singleton (see project_factory_test.cpp)
void EnsureAppSingletons()
{
if (!olive::Core::instance()) {
new olive::Core(olive::Core::CoreParams()); // intentionally leaked
}
if (!olive::DiskManager::instance()) {
olive::DiskManager::CreateInstance();
}
}
void ClearUndoStack()
{
if (olive::Core::instance()) {
olive::Core::instance()->undo_stack()->clear();
}
}
std::unique_ptr<olive::Project> CreateProject()
{
olive::ColorManager::SetUpDefaultConfig();
auto project = std::make_unique<olive::Project>();
project->Initialize();
return project;
}
// Redirects QStandardPaths (used for the sequence preset file, which
// PresetManager rewrites on destruction) to a disposable test location
class StandardPathsTestModeGuard {
public:
StandardPathsTestModeGuard()
{
QStandardPaths::setTestModeEnabled(true);
}
~StandardPathsTestModeGuard()
{
QStandardPaths::setTestModeEnabled(false);
}
};
olive::ClipBlock *CreateClip(olive::Project *project,
const olive::rational &length)
{
auto *clip = new olive::ClipBlock();
clip->setParent(project);
clip->set_length_and_media_out(length);
return clip;
}
olive::Track *CreateTrackWithClip(olive::Project *project,
olive::ClipBlock *clip)
{
auto *track = new olive::Track();
track->setParent(project);
track->AppendBlock(clip);
return track;
}
} // namespace
//
// speedduration
//
TEST(DialogSpeedDuration, InitialValuesReflectSingleClip)
{
EnsureAppSingletons();
auto project = CreateProject();
auto *clip = CreateClip(project.get(), olive::rational(4));
CreateTrackWithClip(project.get(), clip);
olive::SpeedDurationDialog dialog({ clip }, olive::rational(1, 24));
auto *speed_slider = dialog.findChild<olive::FloatSlider *>();
auto *dur_slider = dialog.findChild<olive::RationalSlider *>();
ASSERT_NE(speed_slider, nullptr);
ASSERT_NE(dur_slider, nullptr);
EXPECT_DOUBLE_EQ(speed_slider->GetValue(), 1.0);
EXPECT_EQ(dur_slider->GetValue(), olive::rational(4));
EXPECT_FALSE(speed_slider->IsTristate());
EXPECT_FALSE(dur_slider->IsTristate());
}
TEST(DialogSpeedDuration, LinkedSpeedChangeUpdatesDuration)
{
EnsureAppSingletons();
auto project = CreateProject();
auto *clip = CreateClip(project.get(), olive::rational(4));
CreateTrackWithClip(project.get(), clip);
olive::SpeedDurationDialog dialog({ clip }, olive::rational(1, 24));
auto *speed_slider = dialog.findChild<olive::FloatSlider *>();
auto *dur_slider = dialog.findChild<olive::RationalSlider *>();
// Programmatic SetValue() does not emit ValueChanged (only user edits
// do), so emit the signal explicitly to drive the linked update
speed_slider->SetValue(2.0);
emit speed_slider->ValueChanged(2.0);
EXPECT_EQ(dur_slider->GetValue(), olive::rational(2));
speed_slider->SetValue(0.5);
emit speed_slider->ValueChanged(0.5);
EXPECT_EQ(dur_slider->GetValue(), olive::rational(8));
}
TEST(DialogSpeedDuration, LinkedDurationChangeUpdatesSpeed)
{
EnsureAppSingletons();
auto project = CreateProject();
auto *clip = CreateClip(project.get(), olive::rational(4));
CreateTrackWithClip(project.get(), clip);
olive::SpeedDurationDialog dialog({ clip }, olive::rational(1, 24));
auto *speed_slider = dialog.findChild<olive::FloatSlider *>();
auto *dur_slider = dialog.findChild<olive::RationalSlider *>();
// Programmatic SetValue() does not emit ValueChanged (only user edits
// do), so emit the signal explicitly to drive the linked update
dur_slider->SetValue(olive::rational(2));
emit dur_slider->ValueChanged(olive::rational(2));
EXPECT_DOUBLE_EQ(speed_slider->GetValue(), 2.0);
dur_slider->SetValue(olive::rational(16));
emit dur_slider->ValueChanged(olive::rational(16));
EXPECT_DOUBLE_EQ(speed_slider->GetValue(), 0.25);
}
TEST(DialogSpeedDuration, AcceptAppliesSpeedAndLength)
{
EnsureAppSingletons();
auto project = CreateProject();
auto *clip = CreateClip(project.get(), olive::rational(4));
CreateTrackWithClip(project.get(), clip);
{
olive::SpeedDurationDialog dialog({ clip }, olive::rational(1, 24));
// Doubling the speed with the link checked halves the duration
auto *speed_slider = dialog.findChild<olive::FloatSlider *>();
speed_slider->SetValue(2.0);
emit speed_slider->ValueChanged(2.0);
dialog.accept();
}
EXPECT_DOUBLE_EQ(clip->speed(), 2.0);
EXPECT_EQ(clip->length(), olive::rational(2));
ClearUndoStack();
}
TEST(DialogSpeedDuration, DifferingSpeedsAcrossClipsProduceTristate)
{
EnsureAppSingletons();
auto project = CreateProject();
auto *clip_a = CreateClip(project.get(), olive::rational(4));
auto *clip_b = CreateClip(project.get(), olive::rational(4));
CreateTrackWithClip(project.get(), clip_a);
clip_b->SetStandardValue(olive::ClipBlock::kSpeedInput, 2.0);
CreateTrackWithClip(project.get(), clip_b);
olive::SpeedDurationDialog dialog({ clip_a, clip_b },
olive::rational(1, 24));
EXPECT_TRUE(dialog.findChild<olive::FloatSlider *>()->IsTristate());
// Durations are identical, so the duration slider must not be tristate
EXPECT_FALSE(dialog.findChild<olive::RationalSlider *>()->IsTristate());
}
TEST(DialogSpeedDuration, AcceptDerivesPerClipSpeedFromDuration)
{
EnsureAppSingletons();
auto project = CreateProject();
auto *clip_a = CreateClip(project.get(), olive::rational(4));
auto *clip_b = CreateClip(project.get(), olive::rational(4));
CreateTrackWithClip(project.get(), clip_a);
clip_b->SetStandardValue(olive::ClipBlock::kSpeedInput, 2.0);
CreateTrackWithClip(project.get(), clip_b);
{
olive::SpeedDurationDialog dialog({ clip_a, clip_b },
olive::rational(1, 24));
// Speed is tristate, so accept() must compute each clip's speed
// from its own length/speed ratio: speed = old_speed * old_len / new_len
dialog.findChild<olive::RationalSlider *>()->SetValue(
olive::rational(2));
dialog.accept();
}
EXPECT_EQ(clip_a->length(), olive::rational(2));
EXPECT_EQ(clip_b->length(), olive::rational(2));
EXPECT_DOUBLE_EQ(clip_a->speed(), 2.0);
EXPECT_DOUBLE_EQ(clip_b->speed(), 4.0);
ClearUndoStack();
}
//
// keyframeproperties
//
TEST(DialogKeyframeProperties, SingleKeyAcceptWritesAllFields)
{
EnsureAppSingletons();
auto project = CreateProject();
auto *node = new olive::MathNode();
node->setParent(project.get());
auto *key = new olive::NodeKeyframe(olive::rational(0), 1.0,
olive::NodeKeyframe::kLinear, 0, -1,
olive::MathNode::kParamAIn);
key->setParent(node);
// The dialog stores the keyframe vector by reference, so it must outlive it
std::vector<olive::NodeKeyframe *> keys = { key };
{
olive::KeyframePropertiesDialog dialog(keys, olive::rational(1, 24));
auto *time_slider = dialog.findChild<olive::RationalSlider *>();
auto *type_select = dialog.findChild<QComboBox *>();
auto *bezier_group = dialog.findChild<QGroupBox *>();
ASSERT_NE(time_slider, nullptr);
ASSERT_NE(type_select, nullptr);
ASSERT_NE(bezier_group, nullptr);
// Initial state reflects the keyframe
EXPECT_TRUE(time_slider->isEnabled());
EXPECT_EQ(time_slider->GetValue(), olive::rational(0));
ASSERT_EQ(type_select->count(), 3);
EXPECT_EQ(type_select->currentData().toInt(), olive::NodeKeyframe::kLinear);
EXPECT_FALSE(bezier_group->isEnabled());
// Switching to Bezier enables the bezier handle editors
type_select->setCurrentIndex(2);
ASSERT_EQ(type_select->currentData().toInt(),
olive::NodeKeyframe::kBezier);
EXPECT_TRUE(bezier_group->isEnabled());
time_slider->SetValue(olive::rational(1, 2));
const QList<olive::FloatSlider *> sliders =
dialog.findChildren<olive::FloatSlider *>();
ASSERT_EQ(sliders.size(), 4);
sliders.at(0)->SetValue(0.1); // bezier in x
sliders.at(1)->SetValue(0.2); // bezier in y
sliders.at(2)->SetValue(0.3); // bezier out x
sliders.at(3)->SetValue(0.4); // bezier out y
dialog.accept();
}
EXPECT_EQ(key->time(), olive::rational(1, 2));
EXPECT_EQ(key->type(), olive::NodeKeyframe::kBezier);
EXPECT_DOUBLE_EQ(key->bezier_control_in().x(), 0.1);
EXPECT_DOUBLE_EQ(key->bezier_control_in().y(), 0.2);
EXPECT_DOUBLE_EQ(key->bezier_control_out().x(), 0.3);
EXPECT_DOUBLE_EQ(key->bezier_control_out().y(), 0.4);
ClearUndoStack();
}
TEST(DialogKeyframeProperties, MixedTypesAddPlaceholderItem)
{
EnsureAppSingletons();
auto project = CreateProject();
auto *node = new olive::MathNode();
node->setParent(project.get());
auto *key_a = new olive::NodeKeyframe(olive::rational(0), 1.0,
olive::NodeKeyframe::kLinear, 0, -1,
olive::MathNode::kParamAIn);
auto *key_b = new olive::NodeKeyframe(olive::rational(1), 2.0,
olive::NodeKeyframe::kHold, 0, -1,
olive::MathNode::kParamAIn);
key_a->setParent(node);
key_b->setParent(node);
// The dialog stores the keyframe vector by reference, so it must outlive it
std::vector<olive::NodeKeyframe *> keys = { key_a, key_b };
{
olive::KeyframePropertiesDialog dialog(keys, olive::rational(1, 24));
auto *type_select = dialog.findChild<QComboBox *>();
// An "--" placeholder item with data -1 is prepended for mixed types
ASSERT_EQ(type_select->count(), 4);
EXPECT_EQ(type_select->itemData(0).toInt(), -1);
EXPECT_EQ(type_select->currentIndex(), 0);
dialog.accept();
}
// Accepting with the placeholder selected must not change key types
EXPECT_EQ(key_a->type(), olive::NodeKeyframe::kLinear);
EXPECT_EQ(key_b->type(), olive::NodeKeyframe::kHold);
ClearUndoStack();
}
TEST(DialogKeyframeProperties, KeysOnSameTrackDisableTimeEdit)
{
EnsureAppSingletons();
auto project = CreateProject();
auto *node = new olive::MathNode();
node->setParent(project.get());
auto *key_a = new olive::NodeKeyframe(olive::rational(0), 1.0,
olive::NodeKeyframe::kLinear, 0, -1,
olive::MathNode::kParamAIn);
auto *key_b = new olive::NodeKeyframe(olive::rational(1), 2.0,
olive::NodeKeyframe::kLinear, 0, -1,
olive::MathNode::kParamAIn);
key_a->setParent(node);
key_b->setParent(node);
// The dialog stores the keyframe vector by reference, so it must outlive it
std::vector<olive::NodeKeyframe *> keys = { key_a, key_b };
olive::KeyframePropertiesDialog dialog(keys, olive::rational(1, 24));
// Moving two keys of the same track in time could reorder them, so the
// time editor must be disabled
EXPECT_FALSE(dialog.findChild<olive::RationalSlider *>()->isEnabled());
}
//
// markerproperties
//
TEST(DialogMarkerProperties, SingleMarkerAcceptWritesFields)
{
EnsureAppSingletons();
olive::TimelineMarker marker(
3,
olive::TimeRange(olive::rational(1), olive::rational(2)),
QStringLiteral("Marker A"));
{
olive::MarkerPropertiesDialog dialog({ &marker },
olive::rational(1, 24));
auto *label_edit = dialog.findChild<QLineEdit *>();
auto *color_menu = dialog.findChild<olive::ColorCodingComboBox *>();
const QList<olive::RationalSlider *> sliders =
dialog.findChildren<olive::RationalSlider *>();
ASSERT_NE(label_edit, nullptr);
ASSERT_NE(color_menu, nullptr);
ASSERT_EQ(sliders.size(), 2);
EXPECT_EQ(label_edit->text(), QStringLiteral("Marker A"));
EXPECT_EQ(color_menu->GetSelectedColor(), 3);
EXPECT_EQ(sliders.at(0)->GetValue(), olive::rational(1));
EXPECT_EQ(sliders.at(1)->GetValue(), olive::rational(2));
label_edit->setText(QStringLiteral("Renamed"));
sliders.at(0)->SetValue(olive::rational(1, 2));
sliders.at(1)->SetValue(olive::rational(3, 2));
dialog.accept();
}
EXPECT_EQ(marker.name(), QStringLiteral("Renamed"));
EXPECT_EQ(marker.time().in(), olive::rational(1, 2));
EXPECT_EQ(marker.time().out(), olive::rational(3, 2));
EXPECT_EQ(marker.color(), 3);
ClearUndoStack();
}
TEST(DialogMarkerProperties, MultipleMarkersDisableTimeAndShowPlaceholder)
{
EnsureAppSingletons();
olive::TimelineMarker marker_a(
1,
olive::TimeRange(olive::rational(1), olive::rational(2)),
QStringLiteral("Alpha"));
olive::TimelineMarker marker_b(
2,
olive::TimeRange(olive::rational(5), olive::rational(6)),
QStringLiteral("Beta"));
{
olive::MarkerPropertiesDialog dialog({ &marker_a, &marker_b },
olive::rational(1, 24));
auto *label_edit = dialog.findChild<QLineEdit *>();
auto *color_menu = dialog.findChild<olive::ColorCodingComboBox *>();
const QList<olive::RationalSlider *> sliders =
dialog.findChildren<olive::RationalSlider *>();
// Time cannot be edited for multiple markers
EXPECT_FALSE(sliders.at(0)->isEnabled());
EXPECT_TRUE(sliders.at(0)->IsTristate());
EXPECT_FALSE(sliders.at(1)->isEnabled());
EXPECT_TRUE(sliders.at(1)->IsTristate());
// Differing names show a placeholder instead of text
EXPECT_TRUE(label_edit->text().isEmpty());
EXPECT_FALSE(label_edit->placeholderText().isEmpty());
// Differing colors are represented by -1
EXPECT_EQ(color_menu->GetSelectedColor(), -1);
dialog.accept();
}
// Nothing should have been written back
EXPECT_EQ(marker_a.name(), QStringLiteral("Alpha"));
EXPECT_EQ(marker_b.name(), QStringLiteral("Beta"));
EXPECT_EQ(marker_a.color(), 1);
EXPECT_EQ(marker_b.color(), 2);
ClearUndoStack();
}
//
// sequence
//
TEST(DialogSequencePreset, SaveLoadRoundTrip)
{
olive::SequencePreset preset(QStringLiteral("Test Preset"), 1920, 1080,
olive::rational(24, 1), olive::rational(1, 1),
olive::VideoParams::kInterlacedTopFirst, 48000,
olive::core::kChannelLayoutStereo, 2,
olive::PixelFormat::F16, true);
QByteArray xml;
QBuffer buffer(&xml);
buffer.open(QIODevice::WriteOnly);
QXmlStreamWriter writer(&buffer);
writer.writeStartDocument();
writer.writeStartElement(QStringLiteral("preset"));
preset.Save(&writer);
writer.writeEndElement();
writer.writeEndDocument();
buffer.close();
olive::SequencePreset loaded;
QBuffer read_buffer(&xml);
read_buffer.open(QIODevice::ReadOnly);
QXmlStreamReader reader(&read_buffer);
ASSERT_TRUE(reader.readNextStartElement());
ASSERT_EQ(reader.name().toString(), QStringLiteral("preset"));
loaded.Load(&reader);
EXPECT_EQ(loaded.GetName(), QStringLiteral("Test Preset"));
EXPECT_EQ(loaded.width(), 1920);
EXPECT_EQ(loaded.height(), 1080);
EXPECT_EQ(loaded.frame_rate(), olive::rational(24, 1));
EXPECT_EQ(loaded.pixel_aspect(), olive::rational(1, 1));
EXPECT_EQ(loaded.interlacing(), olive::VideoParams::kInterlacedTopFirst);
EXPECT_EQ(loaded.sample_rate(), 48000);
EXPECT_EQ(loaded.channel_layout(), olive::core::kChannelLayoutStereo);
EXPECT_EQ(loaded.preview_divider(), 2);
EXPECT_EQ(loaded.preview_format(), olive::PixelFormat::F16);
EXPECT_TRUE(loaded.preview_autocache());
}
TEST(DialogSequencePreset, LoadsLegacyInterlacingElement)
{
// Older builds wrote the interlacing element as "interlacing_"; such
// preset files must still load
QByteArray xml = R"(<preset>
<name>Legacy</name>
<width>1280</width>
<height>720</height>
<framerate>24/1</framerate>
<pixelaspect>1/1</pixelaspect>
<interlacing_>2</interlacing_>
<samplerate>44100</samplerate>
<chlayout>3</chlayout>
<divider>1</divider>
<format>4</format>
<autocache>0</autocache>
</preset>)";
olive::SequencePreset loaded;
QBuffer read_buffer(&xml);
read_buffer.open(QIODevice::ReadOnly);
QXmlStreamReader reader(&read_buffer);
ASSERT_TRUE(reader.readNextStartElement());
ASSERT_EQ(reader.name().toString(), QStringLiteral("preset"));
loaded.Load(&reader);
EXPECT_EQ(loaded.GetName(), QStringLiteral("Legacy"));
EXPECT_EQ(loaded.width(), 1280);
EXPECT_EQ(loaded.interlacing(),
static_cast<olive::VideoParams::Interlacing>(2));
}
TEST(DialogSequenceParameterTab, ReflectsSequenceParameters)
{
auto project = CreateProject();
auto *sequence = new olive::Sequence();
sequence->setParent(project.get());
sequence->SetVideoParams(olive::VideoParams(
1920, 1080, olive::rational(1001, 30000), olive::PixelFormat::F32,
olive::VideoParams::kInternalChannelCount, olive::rational(1, 1),
olive::VideoParams::kInterlaceNone, 2));
sequence->SetAudioParams(olive::AudioParams(
48000, olive::core::kChannelLayoutStereo,
olive::Sequence::kDefaultSampleFormat));
olive::SequenceDialogParameterTab tab(sequence);
EXPECT_EQ(tab.GetSelectedVideoWidth(), 1920);
EXPECT_EQ(tab.GetSelectedVideoHeight(), 1080);
EXPECT_EQ(tab.GetSelectedVideoFrameRate(), olive::rational(30000, 1001));
EXPECT_EQ(tab.GetSelectedVideoPixelAspect(), olive::rational(1, 1));
EXPECT_EQ(tab.GetSelectedVideoInterlacingMode(),
olive::VideoParams::kInterlaceNone);
EXPECT_EQ(tab.GetSelectedPreviewResolution(), 2);
EXPECT_EQ(tab.GetSelectedPreviewFormat(), olive::PixelFormat::F32);
EXPECT_EQ(tab.GetSelectedAudioSampleRate(), 48000);
EXPECT_EQ(tab.GetSelectedAudioChannelLayout(),
olive::core::kChannelLayoutStereo);
}
TEST(DialogSequenceParameterTab, PresetChangedAppliesValues)
{
auto project = CreateProject();
auto *sequence = new olive::Sequence();
sequence->setParent(project.get());
sequence->SetVideoParams(olive::VideoParams(
1920, 1080, olive::rational(1, 24), olive::PixelFormat::F32,
olive::VideoParams::kInternalChannelCount, olive::rational(1, 1),
olive::VideoParams::kInterlaceNone, 1));
sequence->SetAudioParams(olive::AudioParams(
48000, olive::core::kChannelLayoutStereo,
olive::Sequence::kDefaultSampleFormat));
olive::SequenceDialogParameterTab tab(sequence);
tab.PresetChanged(olive::SequencePreset(
QStringLiteral("Preset"), 1280, 720, olive::rational(24, 1),
olive::rational(1, 1), olive::VideoParams::kInterlacedTopFirst, 44100,
olive::core::kChannelLayoutStereo, 4, olive::PixelFormat::F16, false));
EXPECT_EQ(tab.GetSelectedVideoWidth(), 1280);
EXPECT_EQ(tab.GetSelectedVideoHeight(), 720);
EXPECT_EQ(tab.GetSelectedVideoFrameRate(), olive::rational(24, 1));
EXPECT_EQ(tab.GetSelectedVideoInterlacingMode(),
olive::VideoParams::kInterlacedTopFirst);
EXPECT_EQ(tab.GetSelectedAudioSampleRate(), 44100);
EXPECT_EQ(tab.GetSelectedPreviewResolution(), 4);
EXPECT_EQ(tab.GetSelectedPreviewFormat(), olive::PixelFormat::F16);
}
TEST(DialogSequenceDialog, AcceptNonUndoableAppliesParameters)
{
StandardPathsTestModeGuard test_mode;
EnsureAppSingletons();
auto project = CreateProject();
auto *sequence = new olive::Sequence();
sequence->setParent(project.get());
sequence->SetLabel(QStringLiteral("Seq A"));
sequence->SetVideoParams(olive::VideoParams(
1920, 1080, olive::rational(1, 24), olive::PixelFormat::F32,
olive::VideoParams::kInternalChannelCount, olive::rational(1, 1),
olive::VideoParams::kInterlaceNone, 1));
sequence->SetAudioParams(olive::AudioParams(
48000, olive::core::kChannelLayoutStereo,
olive::Sequence::kDefaultSampleFormat));
{
olive::SequenceDialog dialog(sequence, olive::SequenceDialog::kExisting);
dialog.SetUndoable(false);
auto *tab = dialog.findChild<olive::SequenceDialogParameterTab *>();
ASSERT_NE(tab, nullptr);
tab->PresetChanged(olive::SequencePreset(
QStringLiteral("Preset"), 1280, 720, olive::rational(24, 1),
olive::rational(1, 1), olive::VideoParams::kInterlacedTopFirst,
44100, olive::core::kChannelLayoutStereo, 4,
olive::PixelFormat::F32, false));
auto *name_field = dialog.findChild<QLineEdit *>();
ASSERT_NE(name_field, nullptr);
name_field->setText(QStringLiteral("Seq B"));
dialog.accept();
EXPECT_EQ(dialog.result(), QDialog::Accepted);
}
EXPECT_EQ(sequence->GetLabel(), QStringLiteral("Seq B"));
EXPECT_EQ(sequence->GetVideoParams().width(), 1280);
EXPECT_EQ(sequence->GetVideoParams().height(), 720);
EXPECT_EQ(sequence->GetVideoParams().frame_rate(), olive::rational(24, 1));
EXPECT_EQ(sequence->GetVideoParams().interlacing(),
olive::VideoParams::kInterlacedTopFirst);
EXPECT_EQ(sequence->GetVideoParams().divider(), 4);
EXPECT_EQ(sequence->GetAudioParams().sample_rate(), 44100);
}
TEST(DialogSequenceDialog, AcceptUndoablePushesCommand)
{
StandardPathsTestModeGuard test_mode;
EnsureAppSingletons();
auto project = CreateProject();
auto *sequence = new olive::Sequence();
sequence->setParent(project.get());
sequence->SetLabel(QStringLiteral("Seq A"));
sequence->SetVideoParams(olive::VideoParams(
1920, 1080, olive::rational(1, 24), olive::PixelFormat::F32,
olive::VideoParams::kInternalChannelCount, olive::rational(1, 1),
olive::VideoParams::kInterlaceNone, 1));
sequence->SetAudioParams(olive::AudioParams(
48000, olive::core::kChannelLayoutStereo,
olive::Sequence::kDefaultSampleFormat));
{
olive::SequenceDialog dialog(sequence, olive::SequenceDialog::kExisting);
auto *tab = dialog.findChild<olive::SequenceDialogParameterTab *>();
ASSERT_NE(tab, nullptr);
tab->PresetChanged(olive::SequencePreset(
QStringLiteral("Preset"), 640, 360, olive::rational(24, 1),
olive::rational(1, 1), olive::VideoParams::kInterlaceNone, 48000,
olive::core::kChannelLayoutStereo, 1, olive::PixelFormat::F32,
false));
dialog.accept();
EXPECT_EQ(dialog.result(), QDialog::Accepted);
}
EXPECT_EQ(sequence->GetVideoParams().width(), 640);
EXPECT_EQ(sequence->GetVideoParams().height(), 360);
ClearUndoStack();
}
TEST(DialogSequenceDialog, PresetTabListsDefaultPresets)
{
StandardPathsTestModeGuard test_mode;
EnsureAppSingletons();
auto project = CreateProject();
auto *sequence = new olive::Sequence();
sequence->setParent(project.get());
olive::SequenceDialog dialog(sequence, olive::SequenceDialog::kExisting);
auto *tree = dialog.findChild<QTreeWidget *>();
ASSERT_NE(tree, nullptr);
// "My Presets" plus the standard preset folders
EXPECT_GE(tree->topLevelItemCount(), 4);
}
//
// footageproperties
//
TEST(DialogFootageProperties, AcceptRenamesAndSetsSourceStartTime)
{
EnsureAppSingletons();
auto project = CreateProject();
auto *footage = new olive::Footage();
footage->setParent(project.get());
footage->SetLabel(QStringLiteral("Clip A"));
footage->set_filename(QStringLiteral("/tmp/oak-nonexistent.mp4"));
{
olive::FootagePropertiesDialog dialog(nullptr, footage);
auto *name_field = dialog.findChild<QLineEdit *>();
auto *start_enable = dialog.findChild<QCheckBox *>();
auto *start_spin = dialog.findChild<QDoubleSpinBox *>();
ASSERT_NE(name_field, nullptr);
ASSERT_NE(start_enable, nullptr);
ASSERT_NE(start_spin, nullptr);
EXPECT_EQ(name_field->text(), QStringLiteral("Clip A"));
EXPECT_FALSE(start_enable->isChecked());
EXPECT_FALSE(start_spin->isEnabled());
name_field->setText(QStringLiteral("Clip B"));
start_enable->setChecked(true);
EXPECT_TRUE(start_spin->isEnabled());
start_spin->setValue(12.5);
// accept() is a private slot, invoke it through the meta-object
QMetaObject::invokeMethod(&dialog, "accept");
}
EXPECT_EQ(footage->GetLabel(), QStringLiteral("Clip B"));
EXPECT_TRUE(footage->HasSourceStartTime());
EXPECT_EQ(footage->source_start_time(), olive::rational(25, 2));
EXPECT_EQ(footage->source_start_time_source(), QStringLiteral("manual"));
{
// Unchecking the box must clear the source start time again
olive::FootagePropertiesDialog dialog(nullptr, footage);
auto *start_enable = dialog.findChild<QCheckBox *>();
ASSERT_NE(start_enable, nullptr);
EXPECT_TRUE(start_enable->isChecked());
start_enable->setChecked(false);
// accept() is a private slot, invoke it through the meta-object
QMetaObject::invokeMethod(&dialog, "accept");
}
EXPECT_FALSE(footage->HasSourceStartTime());
ClearUndoStack();
}
//
// footagerelink
//
TEST(DialogFootageRelink, TableListsFootageAndFilenames)
{
auto project = CreateProject();
auto *footage_a = new olive::Footage();
footage_a->setParent(project.get());
footage_a->SetLabel(QStringLiteral("Footage A"));
footage_a->set_filename(QStringLiteral("/old/path/a.mp4"));
auto *footage_b = new olive::Footage();
footage_b->setParent(project.get());
footage_b->SetLabel(QStringLiteral("Footage B"));
footage_b->set_filename(QStringLiteral("/old/path/b.mp4"));
olive::FootageRelinkDialog dialog({ footage_a, footage_b });
auto *table = dialog.findChild<QTreeWidget *>();
ASSERT_NE(table, nullptr);
ASSERT_EQ(table->topLevelItemCount(), 2);
EXPECT_EQ(table->topLevelItem(0)->text(0), QStringLiteral("Footage A"));
EXPECT_EQ(table->topLevelItem(0)->text(1),
QStringLiteral("/old/path/a.mp4"));
EXPECT_EQ(table->topLevelItem(1)->text(0), QStringLiteral("Footage B"));
EXPECT_EQ(table->topLevelItem(1)->text(1),
QStringLiteral("/old/path/b.mp4"));
}
//
// projectproperties
//
TEST(DialogProjectProperties, OcioValidationTogglesOnInvalidFilename)
{
EnsureAppSingletons();
auto project = CreateProject();
olive::ProjectPropertiesDialog dialog(project.get(), nullptr);
EXPECT_TRUE(dialog.windowTitle().contains(project->name()));
// The first QLineEdit in the dialog is the OCIO config filename
auto *ocio_edit = dialog.findChild<QLineEdit *>();
ASSERT_NE(ocio_edit, nullptr);
// A bad config path flags the line edit as invalid (red text)
ocio_edit->setText(QStringLiteral("/definitely/not/a/config.ocio"));
EXPECT_TRUE(ocio_edit->styleSheet().contains(QStringLiteral("red")));
// Restoring an empty (default) filename clears the error again
ocio_edit->setText(QString());
EXPECT_TRUE(ocio_edit->styleSheet().isEmpty());
// The default config provides selectable input color spaces
bool found_populated_combo = false;
foreach (QComboBox *combo, dialog.findChildren<QComboBox *>()) {
if (combo->count() > 2) {
found_populated_combo = true;
break;
}
}
EXPECT_TRUE(found_populated_combo);
}
TEST(DialogProjectProperties, AcceptWithDefaultsClosesDialog)
{
EnsureAppSingletons();
auto project = CreateProject();
olive::ProjectPropertiesDialog dialog(project.get(), nullptr);
dialog.accept();
EXPECT_EQ(dialog.result(), QDialog::Accepted);
}
+336
View File
@@ -0,0 +1,336 @@
#include <gtest/gtest.h>
#include <QCheckBox>
#include <QComboBox>
#include <QLineEdit>
#include <QMenu>
#include <QSignalSpy>
#include <QStandardPaths>
#include "codec/encoder.h"
#include "dialog/export/codec/h264section.h"
#include "dialog/export/codec/imagesection.h"
#include "dialog/export/exportadvancedvideodialog.h"
#include "dialog/export/exportaudiotab.h"
#include "dialog/export/exportformatcombobox.h"
#include "dialog/export/exportsavepresetdialog.h"
#include "dialog/export/exportsubtitlestab.h"
#include "dialog/export/exportvideotab.h"
#include "node/color/colormanager/colormanager.h"
#include "node/project.h"
namespace
{
// Redirects QStandardPaths (used for the export preset directory) to a
// disposable test location for the lifetime of the guard
class StandardPathsTestModeGuard {
public:
StandardPathsTestModeGuard()
{
QStandardPaths::setTestModeEnabled(true);
}
~StandardPathsTestModeGuard()
{
QStandardPaths::setTestModeEnabled(false);
}
};
QList<int> MenuFormatData(const olive::ExportFormatComboBox &combo)
{
QList<int> formats;
// custom_menu_ is an olive::Menu (a QMenu without its own Q_OBJECT)
auto *menu = combo.findChild<QMenu *>();
if (!menu) {
return formats;
}
foreach (QAction *a, menu->actions()) {
if (!a->isSeparator() && a->data().isValid()) {
formats.append(a->data().toInt());
}
}
return formats;
}
} // namespace
//
// export: format combobox
//
TEST(DialogExportFormatComboBox, GetSetFormatRoundTrip)
{
olive::ExportFormatComboBox combo;
// Before any selection the format is the invalid placeholder
EXPECT_EQ(combo.GetFormat(), olive::ExportFormat::kFormatCount);
combo.SetFormat(olive::ExportFormat::kFormatMatroska);
EXPECT_EQ(combo.GetFormat(), olive::ExportFormat::kFormatMatroska);
EXPECT_EQ(combo.currentText(),
olive::ExportFormat::GetName(olive::ExportFormat::kFormatMatroska));
}
TEST(DialogExportFormatComboBox, MenuSelectionEmitsFormatChanged)
{
olive::ExportFormatComboBox combo;
QSignalSpy spy(&combo, &olive::ExportFormatComboBox::FormatChanged);
QAction action(QStringLiteral("QuickTime"), &combo);
action.setData(static_cast<int>(olive::ExportFormat::kFormatQuickTime));
QMetaObject::invokeMethod(&combo, "HandleIndexChange",
Q_ARG(QAction *, &action));
EXPECT_EQ(combo.GetFormat(), olive::ExportFormat::kFormatQuickTime);
ASSERT_EQ(spy.count(), 1);
EXPECT_EQ(spy.first().first().toInt(),
static_cast<int>(olive::ExportFormat::kFormatQuickTime));
}
TEST(DialogExportFormatComboBox, AudioOnlyModeListsOnlyAudioFormats)
{
olive::ExportFormatComboBox combo(
olive::ExportFormatComboBox::kShowAudioOnly);
const QList<int> formats = MenuFormatData(combo);
EXPECT_FALSE(formats.isEmpty());
foreach (int f, formats) {
const auto fmt = static_cast<olive::ExportFormat::Format>(f);
EXPECT_TRUE(olive::ExportFormat::GetVideoCodecs(fmt).isEmpty())
<< "Format " << f << " should not have video codecs";
EXPECT_FALSE(olive::ExportFormat::GetAudioCodecs(fmt).isEmpty())
<< "Format " << f << " should have audio codecs";
}
EXPECT_TRUE(formats.contains(
static_cast<int>(olive::ExportFormat::kFormatWAV)));
EXPECT_FALSE(formats.contains(
static_cast<int>(olive::ExportFormat::kFormatMatroska)));
}
//
// export: audio tab
//
TEST(DialogExportAudioTab, SetFormatPopulatesCodecs)
{
olive::ExportAudioTab tab;
const QList<olive::ExportCodec::Codec> codecs =
olive::ExportFormat::GetAudioCodecs(olive::ExportFormat::kFormatMatroska);
ASSERT_FALSE(codecs.isEmpty());
EXPECT_EQ(tab.SetFormat(olive::ExportFormat::kFormatMatroska),
codecs.size());
// The first codec is auto-selected
EXPECT_EQ(tab.GetCodec(), codecs.first());
}
TEST(DialogExportAudioTab, LosslessCodecDisablesBitRate)
{
olive::ExportAudioTab tab;
tab.SetFormat(olive::ExportFormat::kFormatMatroska);
tab.SetCodec(olive::ExportCodec::kCodecAAC);
EXPECT_TRUE(tab.bit_rate_slider()->isEnabled());
EXPECT_EQ(tab.GetCodec(), olive::ExportCodec::kCodecAAC);
// PCM is lossless, so no bit rate setting applies
tab.SetCodec(olive::ExportCodec::kCodecPCM);
EXPECT_EQ(tab.GetCodec(), olive::ExportCodec::kCodecPCM);
EXPECT_FALSE(tab.bit_rate_slider()->isEnabled());
EXPECT_TRUE(tab.bit_rate_slider()->IsTristate());
}
TEST(DialogExportAudioTab, FormatWithoutAudioCodecsDisablesTab)
{
olive::ExportAudioTab tab;
// PNG carries no audio
EXPECT_EQ(tab.SetFormat(olive::ExportFormat::kFormatPNG), 0);
EXPECT_FALSE(tab.isEnabled());
}
//
// export: subtitles tab
//
TEST(DialogExportSubtitlesTab, SidecarStateFollowsFormatCapabilities)
{
olive::ExportSubtitlesTab tab;
tab.SetSidecarFormat(olive::ExportFormat::kFormatSRT);
auto *sidecar_box = tab.findChild<QCheckBox *>();
ASSERT_NE(sidecar_box, nullptr);
// Matroska can embed subtitles: sidecar is optional and off by default
tab.SetFormat(olive::ExportFormat::kFormatMatroska);
EXPECT_TRUE(sidecar_box->isEnabled());
EXPECT_FALSE(tab.GetSidecarEnabled());
EXPECT_EQ(tab.GetSubtitleCodec(), olive::ExportCodec::kCodecSRT);
// SetSidecarEnabled toggles the check state (used to restore params)
tab.SetSidecarEnabled(true);
EXPECT_TRUE(tab.GetSidecarEnabled());
tab.SetSidecarEnabled(false);
EXPECT_FALSE(tab.GetSidecarEnabled());
// SRT is a subtitles-only format: sidecar makes no sense, forced off
tab.SetFormat(olive::ExportFormat::kFormatSRT);
EXPECT_FALSE(sidecar_box->isEnabled());
EXPECT_FALSE(tab.GetSidecarEnabled());
// WAV cannot carry subtitles at all: sidecar is forced on
tab.SetFormat(olive::ExportFormat::kFormatWAV);
EXPECT_FALSE(sidecar_box->isEnabled());
EXPECT_TRUE(tab.GetSidecarEnabled());
}
//
// export: video tab
//
TEST(DialogExportVideoTab, SetFormatPopulatesCodecs)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
olive::ExportVideoTab tab(project.color_manager());
const QList<olive::ExportCodec::Codec> codecs =
olive::ExportFormat::GetVideoCodecs(olive::ExportFormat::kFormatMatroska);
ASSERT_FALSE(codecs.isEmpty());
EXPECT_EQ(tab.SetFormat(olive::ExportFormat::kFormatMatroska),
codecs.size());
EXPECT_EQ(tab.GetSelectedCodec(), codecs.first());
tab.SetSelectedCodec(olive::ExportCodec::kCodecH265);
EXPECT_EQ(tab.GetSelectedCodec(), olive::ExportCodec::kCodecH265);
}
TEST(DialogExportVideoTab, CodecSelectsMatchingSection)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
olive::ExportVideoTab tab(project.color_manager());
tab.SetFormat(olive::ExportFormat::kFormatMatroska);
// First Matroska codec is H.264, which has a dedicated section
tab.VideoCodecChanged();
EXPECT_NE(tab.GetCodecSection(), nullptr);
// Still image codecs get the image section instead
tab.SetFormat(olive::ExportFormat::kFormatPNG);
tab.VideoCodecChanged();
EXPECT_NE(dynamic_cast<olive::ImageSection *>(tab.GetCodecSection()),
nullptr);
}
TEST(DialogExportVideoTab, ImageSequenceCheckboxRoundTrips)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
olive::ExportVideoTab tab(project.color_manager());
tab.SetFormat(olive::ExportFormat::kFormatPNG);
tab.VideoCodecChanged();
tab.SetImageSequence(true);
EXPECT_TRUE(tab.IsImageSequenceSet());
tab.SetImageSequence(false);
EXPECT_FALSE(tab.IsImageSequenceSet());
}
TEST(DialogExportVideoTab, MaintainAspectTogglesScalingMethod)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
olive::ExportVideoTab tab(project.color_manager());
tab.maintain_aspect_checkbox()->setChecked(true);
EXPECT_FALSE(tab.scaling_method_combobox()->isEnabled());
tab.maintain_aspect_checkbox()->setChecked(false);
EXPECT_TRUE(tab.scaling_method_combobox()->isEnabled());
}
//
// export: codec sections
//
TEST(DialogExportH264CRFSection, ValueRoundTripsAndClamps)
{
olive::H264CRFSection section(olive::H264CRFSection::kDefaultH264CRF);
EXPECT_EQ(section.GetValue(), olive::H264CRFSection::kDefaultH264CRF);
section.SetValue(30);
EXPECT_EQ(section.GetValue(), 30);
section.SetValue(99);
EXPECT_EQ(section.GetValue(), 51);
section.SetValue(-5);
EXPECT_EQ(section.GetValue(), 0);
}
TEST(DialogExportH264BitRateSection, BitRateRoundTripsInBits)
{
olive::H264BitRateSection section;
section.SetTargetBitRate(8000000);
EXPECT_EQ(section.GetTargetBitRate(), 8000000);
section.SetMaximumBitRate(16000000);
EXPECT_EQ(section.GetMaximumBitRate(), 16000000);
}
//
// export: advanced video dialog
//
TEST(DialogExportAdvancedVideo, FieldsRoundTrip)
{
olive::ExportAdvancedVideoDialog dialog({ QStringLiteral("yuv420p"),
QStringLiteral("yuv422p") });
dialog.set_threads(4);
EXPECT_EQ(dialog.threads(), 4);
dialog.set_pix_fmt(QStringLiteral("yuv422p"));
EXPECT_EQ(dialog.pix_fmt(), QStringLiteral("yuv422p"));
dialog.set_yuv_range(olive::VideoParams::kColorRangeFull);
EXPECT_EQ(dialog.yuv_range(), olive::VideoParams::kColorRangeFull);
}
//
// export: save preset dialog
//
TEST(DialogExportSavePreset, AcceptWritesPresetFile)
{
StandardPathsTestModeGuard test_mode;
olive::EncodingParams params;
olive::ExportSavePresetDialog dialog(params);
auto *name_edit = dialog.findChild<QLineEdit *>();
ASSERT_NE(name_edit, nullptr);
name_edit->setText(QStringLiteral("oak-test-preset"));
EXPECT_EQ(dialog.GetSelectedPresetName(),
QStringLiteral("oak-test-preset"));
dialog.accept();
EXPECT_EQ(dialog.result(), QDialog::Accepted);
EXPECT_TRUE(olive::EncodingParams::GetListOfPresets().contains(
QStringLiteral("oak-test-preset")));
// Clean up the preset file written to the test config location
QFile::remove(QDir(olive::EncodingParams::GetPresetPath())
.filePath(QStringLiteral("oak-test-preset")));
}
+592
View File
@@ -0,0 +1,592 @@
#include <gtest/gtest.h>
#include <memory>
#include <QAction>
#include <QCheckBox>
#include <QComboBox>
#include <QDateTime>
#include <QListWidget>
#include <QMenuBar>
#include <QPlainTextEdit>
#include <QPointer>
#include <QPushButton>
#include <QStackedWidget>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QTreeWidget>
#include "common/filefunctions.h"
#include "config/config.h"
#include "core.h"
#include "dialog/about/about.h"
#include "dialog/actionsearch/actionsearch.h"
#include "dialog/autorecovery/autorecoverydialog.h"
#include "dialog/color/colordialog.h"
#include "dialog/configbase/configdialogbase.h"
#include "dialog/diskcache/diskcachedialog.h"
#include "dialog/preferences/keysequenceeditor.h"
#include "dialog/preferences/tabs/preferencesappearancetab.h"
#include "dialog/preferences/tabs/preferencesdisktab.h"
#include "dialog/preferences/tabs/preferencesluttab.h"
#include "dialog/progress/progress.h"
#include "dialog/rendercancel/rendercancel.h"
#include "dialog/task/task.h"
#include "dialog/text/text.h"
#include "node/color/colormanager/colormanager.h"
#include "node/project.h"
#include "render/diskmanager.h"
namespace
{
void EnsureAppSingletons()
{
if (!olive::Core::instance()) {
new olive::Core(olive::Core::CoreParams()); // intentionally leaked
}
if (!olive::DiskManager::instance()) {
olive::DiskManager::CreateInstance();
}
}
// Redirects QStandardPaths (config/autorecovery locations) to a disposable
// test location for the lifetime of the guard
class StandardPathsTestModeGuard {
public:
StandardPathsTestModeGuard()
{
QStandardPaths::setTestModeEnabled(true);
}
~StandardPathsTestModeGuard()
{
QStandardPaths::setTestModeEnabled(false);
}
};
class DummyTab : public olive::ConfigDialogBaseTab {
public:
bool validate_result = true;
int accept_count = 0;
virtual bool Validate() override
{
return validate_result;
}
virtual void Accept(olive::MultiUndoCommand *) override
{
++accept_count;
}
};
class TestConfigDialog : public olive::ConfigDialogBase {
public:
using olive::ConfigDialogBase::AddTab;
using olive::ConfigDialogBase::ConfigDialogBase;
bool accept_event_called = false;
protected:
virtual void AcceptEvent() override
{
accept_event_called = true;
}
};
class DummyTask : public olive::Task {
public:
DummyTask()
{
SetTitle(QStringLiteral("DummyTask"));
}
protected:
virtual bool Run() override
{
return true;
}
};
} // namespace
//
// about
//
TEST(DialogAbout, WelcomeDialogHasDontShowAgainCheckbox)
{
olive::AboutDialog welcome(true);
EXPECT_NE(welcome.findChild<QCheckBox *>(), nullptr);
olive::AboutDialog about(false);
EXPECT_EQ(about.findChild<QCheckBox *>(), nullptr);
}
TEST(DialogAbout, AcceptWithCheckboxWritesConfig)
{
const QVariant old_value =
olive::Config::Current()[QStringLiteral("ShowWelcomeDialog")];
{
olive::AboutDialog welcome(true);
QCheckBox *box = welcome.findChild<QCheckBox *>();
ASSERT_NE(box, nullptr);
box->setChecked(true);
welcome.accept();
}
EXPECT_FALSE(
olive::Config::Current()[QStringLiteral("ShowWelcomeDialog")].toBool());
olive::Config::Current()[QStringLiteral("ShowWelcomeDialog")] = old_value;
}
//
// actionsearch
//
TEST(DialogActionSearch, SearchFiltersActionsCaseInsensitively)
{
QWidget parent;
olive::ActionSearch dialog(&parent);
QMenuBar bar;
QMenu *file_menu = bar.addMenu(QStringLiteral("&File"));
file_menu->addAction(QStringLiteral("New Project"));
file_menu->addAction(QStringLiteral("&Open..."));
file_menu->addSeparator();
QMenu *recent_menu = file_menu->addMenu(QStringLiteral("Recent"));
recent_menu->addAction(QStringLiteral("Project A"));
bar.addMenu(QStringLiteral("&Edit"))->addAction(QStringLiteral("Undo"));
dialog.SetMenuBar(&bar);
auto *entry = dialog.findChild<olive::ActionSearchEntry *>();
auto *list = dialog.findChild<olive::ActionSearchList *>();
ASSERT_NE(entry, nullptr);
ASSERT_NE(list, nullptr);
entry->setText(QStringLiteral("OPEN"));
ASSERT_EQ(list->count(), 1);
// Item text is "action\n(menu hierarchy)" with accelerator '&' stripped
EXPECT_TRUE(list->item(0)->text().contains(QStringLiteral("Open...")));
EXPECT_TRUE(list->item(0)->text().contains(QStringLiteral("(File)")));
// First match is auto-selected for keyboard use
EXPECT_TRUE(list->item(0)->isSelected());
entry->setText(QStringLiteral("zzzz-no-match"));
EXPECT_EQ(list->count(), 0);
// Nested menus are searched recursively
entry->setText(QStringLiteral("project a"));
ASSERT_EQ(list->count(), 1);
EXPECT_TRUE(list->item(0)->text().contains(QStringLiteral("Recent")));
}
TEST(DialogActionSearch, PerformActionTriggersSelectedAction)
{
QWidget parent;
olive::ActionSearch dialog(&parent);
QMenuBar bar;
QMenu *file_menu = bar.addMenu(QStringLiteral("&File"));
QAction *open_action = file_menu->addAction(QStringLiteral("Open..."));
dialog.SetMenuBar(&bar);
bool triggered = false;
QObject::connect(open_action, &QAction::triggered,
[&triggered]() { triggered = true; });
auto *entry = dialog.findChild<olive::ActionSearchEntry *>();
entry->setText(QStringLiteral("open"));
QMetaObject::invokeMethod(&dialog, "perform_action");
EXPECT_TRUE(triggered);
EXPECT_EQ(dialog.result(), QDialog::Accepted);
}
TEST(DialogActionSearch, SelectionMovesUpAndDown)
{
QWidget parent;
olive::ActionSearch dialog(&parent);
QMenuBar bar;
QMenu *file_menu = bar.addMenu(QStringLiteral("&File"));
file_menu->addAction(QStringLiteral("Alpha"));
file_menu->addAction(QStringLiteral("Beta"));
dialog.SetMenuBar(&bar);
auto *entry = dialog.findChild<olive::ActionSearchEntry *>();
auto *list = dialog.findChild<olive::ActionSearchList *>();
entry->setText(QStringLiteral("a"));
ASSERT_GE(list->count(), 2);
ASSERT_TRUE(list->item(0)->isSelected());
QMetaObject::invokeMethod(&dialog, "move_selection_down");
EXPECT_TRUE(list->item(1)->isSelected());
QMetaObject::invokeMethod(&dialog, "move_selection_up");
EXPECT_TRUE(list->item(0)->isSelected());
// Already at the top: moving up again is a no-op
QMetaObject::invokeMethod(&dialog, "move_selection_up");
EXPECT_TRUE(list->item(0)->isSelected());
}
//
// autorecovery
//
TEST(DialogAutoRecovery, PopulatesTreeFromRecoveryFolders)
{
StandardPathsTestModeGuard test_mode;
EnsureAppSingletons();
const QString root = olive::FileFunctions::GetAutoRecoveryRoot();
const QString folder = QStringLiteral("uuid-abc");
QDir recovery_dir(QDir(root).filePath(folder));
ASSERT_TRUE(recovery_dir.mkpath(QStringLiteral(".")));
QFile realname(recovery_dir.filePath(QStringLiteral("realname.txt")));
ASSERT_TRUE(realname.open(QFile::WriteOnly));
realname.write("My Project");
realname.close();
QFile newest(recovery_dir.filePath(QStringLiteral("1700000000.ove")));
ASSERT_TRUE(newest.open(QFile::WriteOnly));
newest.write("x");
newest.close();
QFile oldest(recovery_dir.filePath(QStringLiteral("1699999000.ove")));
ASSERT_TRUE(oldest.open(QFile::WriteOnly));
oldest.write("x");
oldest.close();
// Non-project files must be ignored
QFile notes(recovery_dir.filePath(QStringLiteral("notes.txt")));
ASSERT_TRUE(notes.open(QFile::WriteOnly));
notes.write("x");
notes.close();
{
olive::AutoRecoveryDialog dialog(QStringLiteral("message"), { folder },
true, nullptr);
auto *tree = dialog.findChild<QTreeWidget *>();
ASSERT_NE(tree, nullptr);
ASSERT_EQ(tree->topLevelItemCount(), 1);
QTreeWidgetItem *top = tree->topLevelItem(0);
// Pretty name comes from realname.txt
EXPECT_EQ(top->text(0), QStringLiteral("My Project"));
ASSERT_EQ(top->childCount(), 2);
// Entries are newest-first; autocheck_latest checks only the newest
EXPECT_EQ(top->child(0)->checkState(0), Qt::Checked);
EXPECT_EQ(top->child(1)->checkState(0), Qt::Unchecked);
// Timestamped filenames are shown as a formatted date/time
EXPECT_EQ(top->child(0)->text(0),
QDateTime::fromSecsSinceEpoch(1700000000).toString());
EXPECT_TRUE(top->child(0)
->data(0, Qt::UserRole)
.toString()
.endsWith(QStringLiteral("1700000000.ove")));
}
{
// Without autocheck_latest nothing is pre-checked
olive::AutoRecoveryDialog dialog(QStringLiteral("message"), { folder },
false, nullptr);
auto *tree = dialog.findChild<QTreeWidget *>();
QTreeWidgetItem *top = tree->topLevelItem(0);
EXPECT_EQ(top->child(0)->checkState(0), Qt::Unchecked);
EXPECT_EQ(top->child(1)->checkState(0), Qt::Unchecked);
// Accepting with nothing checked must not attempt to open anything
dialog.accept();
EXPECT_EQ(dialog.result(), QDialog::Accepted);
}
QDir(root).removeRecursively();
}
TEST(DialogAutoRecovery, MissingRealnameFallsBackToFolderName)
{
StandardPathsTestModeGuard test_mode;
EnsureAppSingletons();
const QString root = olive::FileFunctions::GetAutoRecoveryRoot();
const QString folder = QStringLiteral("uuid-no-realname");
QDir recovery_dir(QDir(root).filePath(folder));
ASSERT_TRUE(recovery_dir.mkpath(QStringLiteral(".")));
QFile recovery(recovery_dir.filePath(QStringLiteral("1700000000.ove")));
ASSERT_TRUE(recovery.open(QFile::WriteOnly));
recovery.write("x");
recovery.close();
olive::AutoRecoveryDialog dialog(QStringLiteral("message"), { folder },
false, nullptr);
auto *tree = dialog.findChild<QTreeWidget *>();
ASSERT_NE(tree, nullptr);
ASSERT_EQ(tree->topLevelItemCount(), 1);
EXPECT_EQ(tree->topLevelItem(0)->text(0), folder);
QDir(root).removeRecursively();
}
//
// configbase
//
TEST(DialogConfigBase, AddTabPopulatesListAndStack)
{
TestConfigDialog dialog;
auto *tab_a = new DummyTab();
auto *tab_b = new DummyTab();
dialog.AddTab(tab_a, QStringLiteral("First"));
dialog.AddTab(tab_b, QStringLiteral("Second"));
auto *list = dialog.findChild<QListWidget *>();
auto *stack = dialog.findChild<QStackedWidget *>();
ASSERT_NE(list, nullptr);
ASSERT_NE(stack, nullptr);
EXPECT_EQ(list->count(), 2);
EXPECT_EQ(stack->count(), 2);
dialog.SetCurrentTab(1);
EXPECT_EQ(list->currentRow(), 1);
EXPECT_EQ(stack->currentIndex(), 1);
}
TEST(DialogConfigBase, AcceptCallsTabsAndAcceptEvent)
{
EnsureAppSingletons();
TestConfigDialog dialog;
auto *tab_a = new DummyTab();
auto *tab_b = new DummyTab();
dialog.AddTab(tab_a, QStringLiteral("First"));
dialog.AddTab(tab_b, QStringLiteral("Second"));
// accept() is a private slot, invoke it through the meta-object
QMetaObject::invokeMethod(&dialog, "accept");
EXPECT_EQ(tab_a->accept_count, 1);
EXPECT_EQ(tab_b->accept_count, 1);
EXPECT_TRUE(dialog.accept_event_called);
EXPECT_EQ(dialog.result(), QDialog::Accepted);
}
TEST(DialogConfigBase, FailedValidateBlocksAccept)
{
EnsureAppSingletons();
TestConfigDialog dialog;
auto *tab_a = new DummyTab();
auto *tab_b = new DummyTab();
tab_a->validate_result = false;
dialog.AddTab(tab_a, QStringLiteral("First"));
dialog.AddTab(tab_b, QStringLiteral("Second"));
// accept() is a private slot, invoke it through the meta-object
QMetaObject::invokeMethod(&dialog, "accept");
EXPECT_EQ(tab_a->accept_count, 0);
EXPECT_EQ(tab_b->accept_count, 0);
EXPECT_FALSE(dialog.accept_event_called);
EXPECT_NE(dialog.result(), QDialog::Accepted);
}
//
// diskcache
//
TEST(DialogDiskCache, AcceptAppliesLimitAndClearOnClose)
{
QTemporaryDir temp_dir;
ASSERT_TRUE(temp_dir.isValid());
olive::DiskCacheFolder folder(temp_dir.path());
olive::DiskCacheDialog dialog(&folder);
auto *limit_slider = dialog.findChild<olive::FloatSlider *>();
auto *clear_box = dialog.findChild<QCheckBox *>();
ASSERT_NE(limit_slider, nullptr);
ASSERT_NE(clear_box, nullptr);
// Fields reflect the folder's current settings (20 GB default)
EXPECT_DOUBLE_EQ(limit_slider->GetValue(), 20.0);
EXPECT_FALSE(clear_box->isChecked());
limit_slider->SetValue(5.0);
clear_box->setChecked(true);
dialog.accept();
EXPECT_EQ(folder.GetLimit(),
5 * static_cast<qint64>(olive::kBytesInGigabyte));
EXPECT_TRUE(folder.GetClearOnClose());
}
//
// text
//
TEST(DialogText, TextIsReadBackFromEditor)
{
olive::TextDialog dialog(QStringLiteral("Hello"));
EXPECT_EQ(dialog.text(), QStringLiteral("Hello"));
auto *edit = dialog.findChild<QPlainTextEdit *>();
ASSERT_NE(edit, nullptr);
edit->setPlainText(QStringLiteral("World"));
EXPECT_EQ(dialog.text(), QStringLiteral("World"));
}
//
// progress
//
TEST(DialogProgress, CancelButtonEmitsCancelledAndDisables)
{
olive::ProgressDialog dialog(QStringLiteral("Working..."),
QStringLiteral("Title"));
EXPECT_EQ(dialog.windowTitle(), QStringLiteral("Title"));
QPushButton *cancel_btn = nullptr;
foreach (QPushButton *btn, dialog.findChildren<QPushButton *>()) {
if (btn->text() == QStringLiteral("Cancel")) {
cancel_btn = btn;
break;
}
}
ASSERT_NE(cancel_btn, nullptr);
bool cancelled = false;
QObject::connect(&dialog, &olive::ProgressDialog::Cancelled,
[&cancelled]() { cancelled = true; });
cancel_btn->click();
EXPECT_TRUE(cancelled);
EXPECT_FALSE(cancel_btn->isEnabled());
}
//
// rendercancel
//
TEST(DialogRenderCancel, IdleWorkersDoNotBlock)
{
olive::RenderCancelDialog dialog;
dialog.SetWorkerCount(2);
dialog.WorkerStarted();
dialog.WorkerDone();
// No busy workers: must return immediately without exec()ing
dialog.RunIfWorkersAreBusy();
EXPECT_FALSE(dialog.isVisible());
}
//
// task
//
TEST(DialogTask, WrapsAndOwnsTask)
{
auto *task = new DummyTask();
QPointer<olive::Task> task_guard(task);
auto *dialog = new olive::TaskDialog(task, QStringLiteral("Title"));
EXPECT_EQ(dialog->GetTask(), task);
EXPECT_EQ(task->parent(), dialog);
// The dialog takes ownership of the task
delete dialog;
EXPECT_TRUE(task_guard.isNull());
}
//
// color
//
TEST(DialogColor, SelectedColorRoundTrips)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
olive::ColorDialog dialog(project.color_manager(),
olive::Color(1.0f, 0.0f, 0.0f, 1.0f));
olive::ManagedColor selected = dialog.GetSelectedColor();
EXPECT_GT(selected.red(), selected.green());
EXPECT_GT(selected.red(), selected.blue());
dialog.SetColor(olive::Color(0.0f, 0.0f, 1.0f, 1.0f));
olive::ManagedColor blue = dialog.GetSelectedColor();
EXPECT_GT(blue.blue(), blue.red());
EXPECT_GT(blue.blue(), blue.green());
}
//
// preferences (remaining tabs)
//
TEST(PreferencesAppearanceTab, ContainsStyleAndColorChoices)
{
olive::PreferencesAppearanceTab tab;
EXPECT_FALSE(tab.findChildren<QComboBox *>().isEmpty());
EXPECT_FALSE(tab.findChildren<olive::ColorCodingComboBox *>().isEmpty());
}
TEST(PreferencesDiskTab, ValidatesUnchangedCacheLocation)
{
EnsureAppSingletons();
olive::PreferencesDiskTab tab;
// Unchanged location must validate without prompting
EXPECT_TRUE(tab.Validate());
}
TEST(PreferencesLutTab, ConstructsWithDirectoryList)
{
olive::PreferencesLutTab tab;
EXPECT_NE(tab.findChild<QListWidget *>(), nullptr);
}
TEST(DialogKeySequenceEditor, TransfersShortcutsToAndFromAction)
{
QAction action(QStringLiteral("Test Action"));
action.setShortcut(QKeySequence(QStringLiteral("Ctrl+T")));
action.setProperty("id", QStringLiteral("test.action"));
action.setProperty("keydefault", QKeySequence(QStringLiteral("Ctrl+T")));
olive::KeySequenceEditor editor(nullptr, &action);
// Editor initializes from the action's current shortcut
EXPECT_EQ(editor.keySequence(), QKeySequence(QStringLiteral("Ctrl+T")));
EXPECT_EQ(editor.action_name(), QStringLiteral("test.action"));
// Shortcut matching the default does not need to be saved
EXPECT_TRUE(editor.export_shortcut().isEmpty());
editor.setKeySequence(QKeySequence(QStringLiteral("Ctrl+U")));
EXPECT_EQ(editor.export_shortcut(),
QStringLiteral("test.action\tCtrl+U"));
// The action is only updated when set_action_shortcut() is called
EXPECT_EQ(action.shortcut(), QKeySequence(QStringLiteral("Ctrl+T")));
editor.set_action_shortcut();
EXPECT_EQ(action.shortcut(), QKeySequence(QStringLiteral("Ctrl+U")));
editor.reset_to_default();
EXPECT_EQ(editor.keySequence(), QKeySequence(QStringLiteral("Ctrl+T")));
}