Files
oak-editor/tests/gtest/widget_standardcombos_test.cpp
T
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

497 lines
15 KiB
C++

#include <gtest/gtest.h>
#include <vector>
#include <QApplication>
#include <QComboBox>
#include <QEvent>
#include <QSignalSpy>
#include "render/videoparams.h"
#include "ui/humanstrings.h"
#include "widget/standardcombos/standardcombos.h"
// These tests complement widget_combos_test.cpp, which already covers item
// counts and basic value round-trips. Here we cover item text/data contents,
// signal emissions, and the stateful edge cases (custom entries, restore
// behavior, language changes).
namespace
{
// QComboBox::currentIndexChanged is overloaded; spy on the int form
QSignalSpy *index_spy(QComboBox *combo)
{
return new QSignalSpy(combo,
static_cast<void (QComboBox::*)(int)>(
&QComboBox::currentIndexChanged));
}
} // namespace
TEST(WidgetStandardCombos, SampleRateTextsAndDataMatchHumanStrings)
{
olive::SampleRateComboBox combo;
ASSERT_EQ(combo.count(),
int(olive::AudioParams::k_supported_sample_rates.size()));
for (int i = 0; i < combo.count(); i++) {
const int rate = olive::AudioParams::k_supported_sample_rates.at(i);
EXPECT_EQ(combo.itemData(i).toInt(), rate);
EXPECT_EQ(combo.itemText(i),
olive::HumanStrings::sample_rate_to_string(rate));
EXPECT_FALSE(combo.itemText(i).isEmpty());
}
}
TEST(WidgetStandardCombos, SampleRateSetEmitsIndexChanged)
{
olive::SampleRateComboBox combo;
combo.set_sample_rate(olive::AudioParams::k_supported_sample_rates.back());
QSignalSpy *spy = index_spy(&combo);
combo.set_sample_rate(olive::AudioParams::k_supported_sample_rates.front());
ASSERT_EQ(spy->count(), 1);
EXPECT_EQ(spy->first().first().toInt(), 0);
delete spy;
}
TEST(WidgetStandardCombos, SampleRateUnknownRateLeavesSelection)
{
olive::SampleRateComboBox combo;
combo.set_sample_rate(olive::AudioParams::k_supported_sample_rates.back());
int sentinel = 7;
while (combo.findData(sentinel) != -1) {
sentinel++;
}
const int old_index = combo.currentIndex();
combo.set_sample_rate(sentinel);
EXPECT_EQ(combo.currentIndex(), old_index);
EXPECT_EQ(combo.get_sample_rate(),
olive::AudioParams::k_supported_sample_rates.back());
}
TEST(WidgetStandardCombos, ChannelLayoutTextsAndDataMatchHumanStrings)
{
olive::ChannelLayoutComboBox combo;
ASSERT_EQ(combo.count(),
int(olive::AudioParams::k_supported_channel_layouts.size()));
for (int i = 0; i < combo.count(); i++) {
const uint64_t layout =
olive::AudioParams::k_supported_channel_layouts.at(i);
EXPECT_EQ(combo.itemData(i).toULongLong(), layout);
EXPECT_EQ(combo.itemText(i),
olive::HumanStrings::channel_layout_to_string(layout));
EXPECT_FALSE(combo.itemText(i).isEmpty());
}
}
TEST(WidgetStandardCombos, ChannelLayoutSetEmitsIndexChanged)
{
olive::ChannelLayoutComboBox combo;
combo.set_channel_layout(
olive::AudioParams::k_supported_channel_layouts.back());
QSignalSpy *spy = index_spy(&combo);
combo.set_channel_layout(
olive::AudioParams::k_supported_channel_layouts.front());
ASSERT_EQ(spy->count(), 1);
EXPECT_EQ(spy->first().first().toInt(), 0);
delete spy;
}
TEST(WidgetStandardCombos, ChannelLayoutUnknownLayoutLeavesSelection)
{
olive::ChannelLayoutComboBox combo;
combo.set_channel_layout(
olive::AudioParams::k_supported_channel_layouts.back());
const uint64_t sentinel = ~uint64_t(0);
for (int i = 0; i < combo.count(); i++) {
ASSERT_NE(combo.itemData(i).toULongLong(), sentinel);
}
const int old_index = combo.currentIndex();
combo.set_channel_layout(sentinel);
EXPECT_EQ(combo.currentIndex(), old_index);
EXPECT_EQ(combo.get_channel_layout(),
olive::AudioParams::k_supported_channel_layouts.back());
}
TEST(WidgetStandardCombos, InterlacedTextsMatchEnumOrder)
{
olive::InterlacedComboBox combo;
ASSERT_EQ(combo.count(), 3);
EXPECT_EQ(combo.itemText(int(olive::VideoParams::k_interlace_none)),
QStringLiteral("None (Progressive)"));
EXPECT_EQ(combo.itemText(int(olive::VideoParams::k_interlaced_top_first)),
QStringLiteral("Top-Field First"));
EXPECT_EQ(
combo.itemText(int(olive::VideoParams::k_interlaced_bottom_first)),
QStringLiteral("Bottom-Field First"));
}
TEST(WidgetStandardCombos, InterlacedSetEmitsIndexChanged)
{
olive::InterlacedComboBox combo;
QSignalSpy *spy = index_spy(&combo);
combo.set_interlace_mode(olive::VideoParams::k_interlaced_bottom_first);
ASSERT_EQ(spy->count(), 1);
EXPECT_EQ(spy->first().first().toInt(),
int(olive::VideoParams::k_interlaced_bottom_first));
delete spy;
}
TEST(WidgetStandardCombos, PixelAspectRatioEntriesCarryNamesAndRatios)
{
olive::PixelAspectRatioComboBox combo;
const QVector<olive::Rational> &standards =
olive::VideoParams::k_standard_pixel_aspects;
const QStringList names =
olive::VideoParams::get_standard_pixel_aspect_ratio_names();
ASSERT_EQ(combo.count(), standards.size() + 1);
ASSERT_EQ(names.size(), standards.size());
for (int i = 0; i < standards.size(); i++) {
EXPECT_EQ(combo.itemData(i).value<olive::Rational>(), standards.at(i));
EXPECT_EQ(combo.itemText(i), names.at(i));
}
// Default selection is the first standard ratio
EXPECT_EQ(combo.currentIndex(), 0);
EXPECT_EQ(combo.get_pixel_aspect_ratio(), standards.first());
}
TEST(WidgetStandardCombos, PixelAspectRatioCustomEntryDefaultsToSquare)
{
olive::PixelAspectRatioComboBox combo;
const int last = combo.count() - 1;
EXPECT_EQ(combo.itemText(last), QStringLiteral("Custom..."));
// A null custom ratio is backed by 1:1 so it can never produce a 0 PAR
const olive::Rational data =
combo.itemData(last).value<olive::Rational>();
EXPECT_DOUBLE_EQ(data.to_double(), 1.0);
}
TEST(WidgetStandardCombos, PixelAspectRatioCustomSetRenamesLastEntry)
{
olive::PixelAspectRatioComboBox combo;
const QVector<olive::Rational> &standards =
olive::VideoParams::k_standard_pixel_aspects;
const int last = combo.count() - 1;
const olive::Rational custom(17, 13);
combo.set_pixel_aspect_ratio(custom);
EXPECT_EQ(combo.currentIndex(), last);
EXPECT_TRUE(
combo.itemText(last).startsWith(QStringLiteral("Custom (")));
EXPECT_EQ(combo.itemData(last).value<olive::Rational>(), custom);
// Returning to a standard ratio selects it but keeps the custom label
combo.set_pixel_aspect_ratio(standards.first());
EXPECT_EQ(combo.currentIndex(), 0);
EXPECT_EQ(combo.get_pixel_aspect_ratio(), standards.first());
EXPECT_TRUE(
combo.itemText(last).startsWith(QStringLiteral("Custom (")));
}
TEST(WidgetStandardCombos, PixelAspectRatioSelectingStandardDoesNotPrompt)
{
olive::PixelAspectRatioComboBox combo;
const QVector<olive::Rational> &standards =
olive::VideoParams::k_standard_pixel_aspects;
ASSERT_GE(standards.size(), 2);
// A user-style index change to a standard entry must not open the
// custom-ratio dialog (selecting the last entry would, so it is
// intentionally not exercised under the offscreen platform)
combo.setCurrentIndex(1);
EXPECT_EQ(combo.get_pixel_aspect_ratio(), standards.at(1));
EXPECT_EQ(combo.itemText(combo.count() - 1),
QStringLiteral("Custom..."));
}
TEST(WidgetStandardCombos, PixelFormatEntriesOrderedWithNames)
{
olive::PixelFormatComboBox combo(false);
// Preview formats u8..f32 are added in enum order
ASSERT_EQ(combo.count(), int(olive::core::PixelFormat::count));
for (int i = 0; i < combo.count(); i++) {
EXPECT_EQ(combo.itemData(i).toInt(), i);
const auto fmt = static_cast<olive::core::PixelFormat::Format>(i);
EXPECT_EQ(combo.itemText(i),
olive::VideoParams::get_format_name(fmt));
EXPECT_FALSE(combo.itemText(i).isEmpty());
}
// Default selection is the first (u8) format
EXPECT_EQ(static_cast<olive::core::PixelFormat::Format>(
combo.get_pixel_format()),
olive::core::PixelFormat::u8);
}
TEST(WidgetStandardCombos, PixelFormatSetEmitsIndexChanged)
{
olive::PixelFormatComboBox combo(false);
QSignalSpy *spy = index_spy(&combo);
combo.set_pixel_format(olive::core::PixelFormat::f32);
ASSERT_EQ(spy->count(), 1);
EXPECT_EQ(spy->first().first().toInt(),
int(olive::core::PixelFormat::f32));
delete spy;
}
TEST(WidgetStandardCombos, PixelFormatFloatOnlyIgnoresIntegerFormat)
{
olive::PixelFormatComboBox combo(true);
ASSERT_GT(combo.count(), 0);
// u8 is not a float format, so it cannot be selected here
combo.set_pixel_format(olive::core::PixelFormat::u8);
olive::core::PixelFormat selected =
static_cast<olive::core::PixelFormat::Format>(
combo.get_pixel_format());
EXPECT_TRUE(selected.is_float());
EXPECT_EQ(combo.currentIndex(), 0);
}
TEST(WidgetStandardCombos, SampleFormatStartsEmpty)
{
olive::SampleFormatComboBox combo;
EXPECT_EQ(combo.count(), 0);
}
TEST(WidgetStandardCombos, SampleFormatAvailableFormatsPopulateInOrder)
{
using SampleFormat = olive::core::SampleFormat;
olive::SampleFormatComboBox combo;
const std::vector<SampleFormat> formats = {
SampleFormat::u8, SampleFormat::s16, SampleFormat::f32
};
combo.set_available_formats(formats);
ASSERT_EQ(combo.count(), int(formats.size()));
for (int i = 0; i < combo.count(); i++) {
EXPECT_EQ(combo.itemData(i).toInt(),
int(static_cast<SampleFormat::Format>(formats.at(i))));
EXPECT_EQ(combo.itemText(i),
olive::HumanStrings::format_to_string(formats.at(i)));
}
// The first entry is selected by default
EXPECT_EQ(static_cast<SampleFormat::Format>(combo.get_sample_format()),
SampleFormat::u8);
}
TEST(WidgetStandardCombos, SampleFormatRestoreDisabledReselectsFirst)
{
using Format = olive::core::SampleFormat::Format;
olive::SampleFormatComboBox combo;
combo.set_attempt_to_restore_format(false);
combo.set_packed_formats();
combo.set_sample_format(olive::core::SampleFormat::f32);
ASSERT_EQ(static_cast<Format>(combo.get_sample_format()),
olive::core::SampleFormat::f32);
// With restore disabled, repopulating falls back to the first entry
combo.set_packed_formats();
EXPECT_EQ(combo.currentIndex(), 0);
EXPECT_EQ(static_cast<Format>(combo.get_sample_format()),
static_cast<Format>(olive::core::SampleFormat::packed_start));
}
TEST(WidgetStandardCombos, SampleFormatSetEmitsIndexChanged)
{
olive::SampleFormatComboBox combo;
combo.set_packed_formats();
QSignalSpy *spy = index_spy(&combo);
combo.set_sample_format(olive::core::SampleFormat::f32);
ASSERT_EQ(spy->count(), 1);
EXPECT_EQ(spy->first().first().toInt(),
int(olive::core::SampleFormat::f32) -
int(olive::core::SampleFormat::packed_start));
delete spy;
}
TEST(WidgetStandardCombos, VideoDividerTextsAndDataMatch)
{
olive::VideoDividerComboBox combo;
ASSERT_EQ(combo.count(), olive::VideoParams::k_supported_dividers.size());
for (int i = 0; i < combo.count(); i++) {
const int divider = olive::VideoParams::k_supported_dividers.at(i);
EXPECT_EQ(combo.itemData(i).toInt(), divider);
EXPECT_EQ(combo.itemText(i),
olive::VideoParams::get_name_for_divider(divider));
EXPECT_FALSE(combo.itemText(i).isEmpty());
}
}
TEST(WidgetStandardCombos, VideoDividerSetEmitsIndexChanged)
{
olive::VideoDividerComboBox combo;
combo.set_divider(olive::VideoParams::k_supported_dividers.last());
QSignalSpy *spy = index_spy(&combo);
combo.set_divider(olive::VideoParams::k_supported_dividers.first());
ASSERT_EQ(spy->count(), 1);
EXPECT_EQ(spy->first().first().toInt(), 0);
delete spy;
}
TEST(WidgetStandardCombos, VideoDividerUnknownDividerLeavesSelection)
{
olive::VideoDividerComboBox combo;
combo.set_divider(olive::VideoParams::k_supported_dividers.last());
int sentinel = 3;
while (combo.findData(sentinel) != -1) {
sentinel += 2;
}
const int old_index = combo.currentIndex();
combo.set_divider(sentinel);
EXPECT_EQ(combo.currentIndex(), old_index);
EXPECT_EQ(combo.get_divider(),
olive::VideoParams::k_supported_dividers.last());
}
TEST(WidgetStandardCombos, FrameRateHasTrailingCustomEntry)
{
olive::FrameRateComboBox combo;
QComboBox *inner = combo.findChild<QComboBox *>();
ASSERT_NE(inner, nullptr);
const QVector<olive::Rational> &standards =
olive::VideoParams::k_supported_frame_rates;
ASSERT_EQ(inner->count(), standards.size() + 1);
for (int i = 0; i < standards.size(); i++) {
EXPECT_EQ(inner->itemData(i).value<olive::Rational>(),
standards.at(i));
EXPECT_EQ(inner->itemText(i),
olive::VideoParams::frame_rate_to_string(standards.at(i)));
}
EXPECT_EQ(inner->itemText(standards.size()),
QStringLiteral("Custom..."));
}
TEST(WidgetStandardCombos, FrameRateProgrammaticSetDoesNotEmit)
{
olive::FrameRateComboBox combo;
QSignalSpy spy(&combo, &olive::FrameRateComboBox::frame_rate_changed);
combo.set_frame_rate(olive::VideoParams::k_supported_frame_rates.at(1));
combo.set_frame_rate(olive::Rational(27, 2));
combo.set_frame_rate(olive::VideoParams::k_supported_frame_rates.at(0));
EXPECT_EQ(spy.count(), 0);
}
TEST(WidgetStandardCombos, FrameRateUserSelectionEmitsFrameRateChanged)
{
olive::FrameRateComboBox combo;
QComboBox *inner = combo.findChild<QComboBox *>();
ASSERT_NE(inner, nullptr);
ASSERT_GE(olive::VideoParams::k_supported_frame_rates.size(), 3);
QSignalSpy spy(&combo, &olive::FrameRateComboBox::frame_rate_changed);
// Standard entries emit directly; only the last "Custom..." entry would
// open a modal input dialog, which is not exercised here
inner->setCurrentIndex(2);
ASSERT_EQ(spy.count(), 1);
EXPECT_EQ(spy.first().first().value<olive::Rational>(),
olive::VideoParams::k_supported_frame_rates.at(2));
EXPECT_EQ(combo.get_frame_rate(),
olive::VideoParams::k_supported_frame_rates.at(2));
}
TEST(WidgetStandardCombos, FrameRateCustomSetRenamesLastEntry)
{
olive::FrameRateComboBox combo;
QComboBox *inner = combo.findChild<QComboBox *>();
ASSERT_NE(inner, nullptr);
const int last = inner->count() - 1;
combo.set_frame_rate(olive::Rational(27, 2));
EXPECT_EQ(inner->currentIndex(), last);
EXPECT_TRUE(
inner->itemText(last).startsWith(QStringLiteral("Custom (")));
EXPECT_EQ(combo.get_frame_rate(), olive::Rational(27, 2));
// A second custom rate replaces the label's rate
combo.set_frame_rate(olive::Rational(29, 2));
EXPECT_EQ(inner->currentIndex(), last);
EXPECT_EQ(combo.get_frame_rate(), olive::Rational(29, 2));
// User-selecting a standard rate afterwards emits and returns it
QSignalSpy spy(&combo, &olive::FrameRateComboBox::frame_rate_changed);
inner->setCurrentIndex(0);
ASSERT_EQ(spy.count(), 1);
EXPECT_EQ(spy.first().first().value<olive::Rational>(),
olive::VideoParams::k_supported_frame_rates.first());
EXPECT_EQ(combo.get_frame_rate(),
olive::VideoParams::k_supported_frame_rates.first());
}
TEST(WidgetStandardCombos, FrameRateLanguageChangePreservesCustom)
{
olive::FrameRateComboBox combo;
combo.set_frame_rate(olive::Rational(27, 2));
QEvent ev(QEvent::LanguageChange);
QApplication::sendEvent(&combo, &ev);
QComboBox *inner = combo.findChild<QComboBox *>();
ASSERT_NE(inner, nullptr);
const QVector<olive::Rational> &standards =
olive::VideoParams::k_supported_frame_rates;
ASSERT_EQ(inner->count(), standards.size() + 1);
EXPECT_EQ(inner->currentIndex(), standards.size());
EXPECT_EQ(combo.get_frame_rate(), olive::Rational(27, 2));
EXPECT_TRUE(inner->itemText(standards.size())
.startsWith(QStringLiteral("Custom (")));
}