Files
oak-editor/tests/gtest/widget_sliderbase_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

437 lines
12 KiB
C++

#include <gtest/gtest.h>
#include <QApplication>
#include <QLabel>
#include <QSignalSpy>
#include "common/configwrapper.h"
#include "widget/focusablelineedit/focusablelineedit.h"
#include "widget/slider/base/numericsliderbase.h"
#include "widget/slider/base/sliderlabel.h"
#include "widget/slider/base/sliderladder.h"
#include "widget/slider/sliderdisplaytypeapp.h"
namespace
{
// NumericSliderBase is abstract (SliderBase's value_to_string /
// string_to_value / value_signal_event are pure), so test it through a
// minimal concrete probe that also exposes the protected entry points
class ProbeNumericSlider : public olive::NumericSliderBase {
public:
void set_value(const QVariant &v)
{
set_value_internal(v);
}
QVariant get_value() const
{
return get_value_internal();
}
QVariant get_offset_public() const
{
return get_offset();
}
QVariant adjust_drag_public(const QVariant &start, const double &drag) const
{
return adjust_drag_distance_internal(start, drag);
}
bool greater_public(const QVariant &lhs, const QVariant &rhs) const
{
return value_greater_than(lhs, rhs);
}
bool less_public(const QVariant &lhs, const QVariant &rhs) const
{
return value_less_than(lhs, rhs);
}
bool can_set_value_public() const
{
return can_set_value();
}
void set_minimum_public(const QVariant &v)
{
set_minimum_internal(v);
}
void set_maximum_public(const QVariant &v)
{
set_maximum_internal(v);
}
QVector<QVariant> signaled;
protected:
virtual QString value_to_string(const QVariant &v) const override
{
return QString::number(v.toDouble());
}
virtual QVariant string_to_value(const QString &s, bool *ok) const override
{
return s.toDouble(ok);
}
virtual void value_signal_event(const QVariant &value) override
{
signaled.append(value);
}
};
// Forces ladder mode on for the duration of a test. Ladder mode keeps the
// SliderLadder away from the macOS cursor-grab path
// (CGAssociateMouseAndMouseCursorPosition), which would otherwise decouple
// the host's real cursor while a ladder widget exists
struct LadderConfigGuard {
LadderConfigGuard()
: old_(OAK_CONFIG("UseSliderLadders").toBool())
{
OAK_CONFIG("UseSliderLadders") = true;
}
~LadderConfigGuard()
{
OAK_CONFIG("UseSliderLadders") = old_;
}
bool old_;
};
// The drag ladder is an orphan top-level popup; dig it out of the
// application's top-level widget list
olive::SliderLadder *find_ladder()
{
const QWidgetList tops = QApplication::topLevelWidgets();
for (QWidget *w : tops) {
if (auto *l = qobject_cast<olive::SliderLadder *>(w)) {
return l;
}
}
return nullptr;
}
// Starts a ladder drag on the slider by emitting its label's press signal
void press_label(ProbeNumericSlider &s)
{
olive::SliderLabel *label = s.findChild<olive::SliderLabel *>();
ASSERT_NE(label, nullptr);
ASSERT_TRUE(QMetaObject::invokeMethod(label, "label_pressed"));
}
} // namespace
TEST(SliderDisplayType, OrdinalValuesAreAbiStable)
{
// These cross the engine C ABI as ints inside node input properties; the
// ordinals must match engine node/sliderdisplaytype.h
EXPECT_EQ(int(olive::slider::k_normal), 0);
EXPECT_EQ(int(olive::slider::k_decibel), 1);
EXPECT_EQ(int(olive::slider::k_percentage), 2);
EXPECT_EQ(int(olive::slider::k_time), 0);
EXPECT_EQ(int(olive::slider::k_float), 1);
EXPECT_EQ(int(olive::slider::k_rational), 2);
}
TEST(NumericSliderBase, DefaultStateIsIdle)
{
ProbeNumericSlider s;
EXPECT_FALSE(s.is_dragging());
EXPECT_TRUE(s.can_set_value_public());
EXPECT_EQ(s.cursor().shape(), Qt::SizeHorCursor);
}
TEST(NumericSliderBase, OffsetRoundTrips)
{
ProbeNumericSlider s;
EXPECT_FALSE(s.get_offset_public().isValid());
s.set_offset(7);
EXPECT_EQ(s.get_offset_public().toInt(), 7);
s.set_offset(-2.5);
EXPECT_DOUBLE_EQ(s.get_offset_public().toDouble(), -2.5);
}
TEST(NumericSliderBase, DragDistanceDefaultsToAddition)
{
ProbeNumericSlider s;
EXPECT_DOUBLE_EQ(s.adjust_drag_public(1.5, 2.25).toDouble(), 3.75);
EXPECT_DOUBLE_EQ(s.adjust_drag_public(1.5, -0.5).toDouble(), 1.0);
}
TEST(NumericSliderBase, ValueComparisonUsesNumericValues)
{
ProbeNumericSlider s;
EXPECT_TRUE(s.greater_public(2.5, 2.4));
EXPECT_FALSE(s.greater_public(1.0, 1.0));
EXPECT_TRUE(s.less_public(1, 2));
// String variants compare by their numeric value, not lexicographically
EXPECT_TRUE(s.less_public(QStringLiteral("9"), QStringLiteral("10")));
}
TEST(NumericSliderBase, SetMinimumClampsExistingValue)
{
ProbeNumericSlider s;
s.set_value(5.0);
s.set_minimum_public(10.0);
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 10.0);
// New values below the minimum clamp on the way in; programmatic sets do
// not emit the value-changed signal
s.set_value(3.0);
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 10.0);
EXPECT_TRUE(s.signaled.isEmpty());
s.set_value(42.0);
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 42.0);
}
TEST(NumericSliderBase, SetMaximumClampsExistingValue)
{
ProbeNumericSlider s;
s.set_value(5.0);
s.set_maximum_public(2.0);
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 2.0);
s.set_value(99.0);
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 2.0);
s.set_value(-7.0);
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), -7.0);
}
TEST(NumericSliderBase, LadderDragUpdatesValueAndSignals)
{
LadderConfigGuard guard;
{
ProbeNumericSlider s;
s.set_ladder_element_count(2);
s.set_value(10.0);
press_label(s);
ASSERT_TRUE(s.is_dragging());
EXPECT_FALSE(s.can_set_value_public());
olive::SliderLadder *ladder = find_ladder();
ASSERT_NE(ladder, nullptr);
// Two outer values either side plus the center entry
EXPECT_EQ(ladder->findChildren<olive::SliderLadderElement *>().size(), 5);
// External sets are blocked while the drag owns the value
s.set_value(50.0);
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 10.0);
EXPECT_TRUE(s.signaled.isEmpty());
// value * multiplier accumulates onto the drag start value
ASSERT_TRUE(QMetaObject::invokeMethod(
ladder, "dragged_by_value", Q_ARG(int, 3), Q_ARG(double, 2.0)));
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 16.0);
ASSERT_EQ(s.signaled.size(), 1);
EXPECT_DOUBLE_EQ(s.signaled.last().toDouble(), 16.0);
ASSERT_TRUE(QMetaObject::invokeMethod(
ladder, "dragged_by_value", Q_ARG(int, 1), Q_ARG(double, 1.0)));
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 17.0);
ASSERT_EQ(s.signaled.size(), 2);
// Releasing after a drag re-signals the final value and ends the drag
ladder->close();
EXPECT_FALSE(s.is_dragging());
EXPECT_TRUE(s.can_set_value_public());
ASSERT_EQ(s.signaled.size(), 3);
EXPECT_DOUBLE_EQ(s.signaled.last().toDouble(), 17.0);
// Programmatic sets are accepted again
s.set_value(3.0);
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 3.0);
}
// Flush the ladder's deleteLater and its queued drag-timer start only
// after the slider is gone, so a stray timer tick can't reach a ladder
// whose owner no longer exists
QCoreApplication::processEvents();
}
TEST(NumericSliderBase, LadderDragClampsToRange)
{
LadderConfigGuard guard;
{
ProbeNumericSlider s;
s.set_ladder_element_count(2);
s.set_minimum_public(0.0);
s.set_maximum_public(8.0);
s.set_value(5.0);
press_label(s);
olive::SliderLadder *ladder = find_ladder();
ASSERT_NE(ladder, nullptr);
ASSERT_TRUE(QMetaObject::invokeMethod(
ladder, "dragged_by_value", Q_ARG(int, -10), Q_ARG(double, 1.0)));
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 0.0);
ASSERT_TRUE(QMetaObject::invokeMethod(
ladder, "dragged_by_value", Q_ARG(int, 100), Q_ARG(double, 1.0)));
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 8.0);
ladder->close();
}
QCoreApplication::processEvents();
}
TEST(NumericSliderBase, ClickWithoutDragShowsEditor)
{
LadderConfigGuard guard;
{
ProbeNumericSlider s;
s.set_ladder_element_count(2);
s.set_value(4.0);
press_label(s);
olive::SliderLadder *ladder = find_ladder();
ASSERT_NE(ladder, nullptr);
// A press/release with no drag in between is treated as a click and
// opens the line editor instead of changing the value
ladder->close();
EXPECT_FALSE(s.is_dragging());
EXPECT_TRUE(s.signaled.isEmpty());
olive::FocusableLineEdit *editor =
s.findChild<olive::FocusableLineEdit *>();
ASSERT_NE(editor, nullptr);
EXPECT_EQ(s.currentWidget(), editor);
EXPECT_EQ(editor->text(), QStringLiteral("4"));
EXPECT_DOUBLE_EQ(s.get_value().toDouble(), 4.0);
}
QCoreApplication::processEvents();
}
TEST(SliderLadder, BuildsElementPerPowerOfTen)
{
LadderConfigGuard guard;
olive::SliderLadder ladder(1.0, 2, QStringLiteral("9999"));
EXPECT_TRUE(ladder.windowFlags() & Qt::Popup);
const auto elements = ladder.findChildren<olive::SliderLadderElement *>();
ASSERT_EQ(elements.size(), 5);
// Coarse multipliers on top, the unit entry in the middle, fine below
EXPECT_DOUBLE_EQ(elements.at(0)->get_multiplier(), 100.0);
EXPECT_DOUBLE_EQ(elements.at(1)->get_multiplier(), 10.0);
EXPECT_DOUBLE_EQ(elements.at(2)->get_multiplier(), 1.0);
EXPECT_DOUBLE_EQ(elements.at(3)->get_multiplier(), 0.1);
EXPECT_DOUBLE_EQ(elements.at(4)->get_multiplier(), 0.01);
// Only the center entry starts highlighted
for (int i = 0; i < elements.size(); i++) {
const QPalette::ColorRole expected =
(i == 2) ? QPalette::Highlight : QPalette::Window;
EXPECT_EQ(elements.at(i)->backgroundRole(), expected) << i;
}
}
TEST(SliderLadder, DragMultiplierScalesElements)
{
LadderConfigGuard guard;
olive::SliderLadder ladder(2.5, 1, QStringLiteral("9999"));
const auto elements = ladder.findChildren<olive::SliderLadderElement *>();
ASSERT_EQ(elements.size(), 3);
EXPECT_DOUBLE_EQ(elements.at(0)->get_multiplier(), 25.0);
EXPECT_DOUBLE_EQ(elements.at(1)->get_multiplier(), 2.5);
EXPECT_DOUBLE_EQ(elements.at(2)->get_multiplier(), 0.25);
}
TEST(SliderLadder, SetValueShowsOnHighlightedElementOnly)
{
LadderConfigGuard guard;
olive::SliderLadder ladder(1.0, 1, QStringLiteral("9999"));
ladder.set_value(QStringLiteral("42"));
const auto elements = ladder.findChildren<olive::SliderLadderElement *>();
ASSERT_EQ(elements.size(), 3);
for (int i = 0; i < elements.size(); i++) {
QLabel *label = elements.at(i)->findChild<QLabel *>();
ASSERT_NE(label, nullptr);
if (i == 1) {
EXPECT_EQ(label->text(), QStringLiteral("1\n42"));
} else {
EXPECT_FALSE(label->text().contains(QStringLiteral("42")));
}
}
}
TEST(SliderLadder, CloseEmitsReleased)
{
LadderConfigGuard guard;
olive::SliderLadder ladder(1.0, 1, QStringLiteral("9999"));
ladder.show();
QSignalSpy spy(&ladder, &olive::SliderLadder::released);
ladder.close();
EXPECT_EQ(spy.count(), 1);
}
TEST(SliderLadder, SingleElementHidesMultiplier)
{
LadderConfigGuard guard;
// With no outer values the ladder is a single readout, and its multiplier
// is hidden since there's nothing to switch between
olive::SliderLadder ladder(1.0, 0, QStringLiteral("9999"));
const auto elements = ladder.findChildren<olive::SliderLadderElement *>();
ASSERT_EQ(elements.size(), 1);
ladder.set_value(QStringLiteral("7"));
QLabel *label = elements.first()->findChild<QLabel *>();
ASSERT_NE(label, nullptr);
EXPECT_EQ(label->text(), QStringLiteral("7"));
}
TEST(SliderLadderElement, LabelReflectsHighlightAndMultiplierVisibility)
{
olive::SliderLadderElement e(2.5, QStringLiteral("9999"));
QLabel *label = e.findChild<QLabel *>();
ASSERT_NE(label, nullptr);
// Unhighlighted: only the multiplier shows, the value slot stays empty
EXPECT_EQ(label->text(), QStringLiteral("2.5\n"));
EXPECT_EQ(e.backgroundRole(), QPalette::Window);
e.set_value(QStringLiteral("9"));
EXPECT_EQ(label->text(), QStringLiteral("2.5\n"));
e.set_highlighted(true);
EXPECT_EQ(label->text(), QStringLiteral("2.5\n9"));
EXPECT_EQ(e.backgroundRole(), QPalette::Highlight);
e.set_highlighted(false);
EXPECT_EQ(e.backgroundRole(), QPalette::Window);
e.set_multiplier_visible(false);
EXPECT_EQ(label->text(), QStringLiteral("9"));
}