Add widget tests for macro tree

Safeguard for rework to support nested groups.
Two things to change with rework:
* Dropping onto a collapsed group appends instead of prepending
* Group running state is not updated right away when there is a
  collapsed group above it for some reason
This commit is contained in:
WarmUpTill
2026-10-21 20:44:09 +02:00
committed by WarmUpTill
parent 621c604262
commit 60e5475642
6 changed files with 1459 additions and 1 deletions

View File

@@ -179,6 +179,19 @@ target_sources(
${ADVSS_SOURCE_DIR}/lib/variables/variable-spinbox.cpp
${ADVSS_SOURCE_DIR}/lib/variables/variable-string.cpp)
# --- macro-tree --- #
target_sources(
${PROJECT_NAME}
PRIVATE test-macro-tree.cpp
ui-test-helpers.cpp
ui-test-helpers.hpp
stubs/macro-undo-redo.cpp
${ADVSS_SOURCE_DIR}/lib/macro/macro-search.cpp
${ADVSS_SOURCE_DIR}/lib/macro/macro-tree.cpp
${ADVSS_SOURCE_DIR}/lib/macro/macro-tree.hpp
${ADVSS_SOURCE_DIR}/lib/variables/variable-usage.cpp)
# --- macro-condition-file --- #
target_include_directories(${PROJECT_NAME}
@@ -232,6 +245,26 @@ if(WIN32)
$<TARGET_RUNTIME_DLLS:${PROJECT_NAME}> $<TARGET_FILE_DIR:${PROJECT_NAME}>
COMMAND_EXPAND_LISTS)
# Platform plugins are not part of TARGET_RUNTIME_DLLS but are needed to
# create the QApplication used by the widget tests
get_target_property(_qt_core_lib Qt::Core IMPORTED_LOCATION_RELEASE)
if(NOT _qt_core_lib)
get_target_property(_qt_core_lib Qt::Core IMPORTED_LOCATION_RELWITHDEBINFO)
endif()
if(_qt_core_lib)
get_filename_component(_qt_bin "${_qt_core_lib}" DIRECTORY)
get_filename_component(_qt_platforms "${_qt_bin}/../plugins/platforms"
ABSOLUTE)
add_custom_command(
TARGET ${PROJECT_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_qt_platforms}"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/platforms")
else()
message(WARNING "Could not determine Qt platform plugin directory - "
"widget tests might fail to start")
endif()
# Copy remaining OBS deps (zlib, srt, librist, ffmpeg, etc.) that are not
# CMake targets but are required at runtime. Derive the path from ZLIB::ZLIB
# which lives in the same deps directory.

View File

@@ -0,0 +1,8 @@
#include "macro-undo-redo.hpp"
namespace advss {
void RegisterGroupCreateUndoRedo(const std::string &) {}
void RegisterGroupRemoveUndoRedo(const std::string &) {}
} // namespace advss

1038
tests/test-macro-tree.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,20 @@
#define CATCH_CONFIG_MAIN
#define CATCH_CONFIG_RUNNER
#include "catch.hpp"
#include <QApplication>
#include <iostream>
int main(int argc, char *argv[])
{
// The OBS Qt deps do not ship the offscreen platform plugin on all
// platforms, so it is only used as a fallback.
if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) {
qputenv("QT_QPA_PLATFORM", "minimal;offscreen");
}
QApplication app(argc, argv);
std::cout << "Using Qt platform \""
<< QGuiApplication::platformName().toStdString() << "\"\n";
return Catch::Session().run(argc, argv);
}

285
tests/ui-test-helpers.cpp Normal file
View File

@@ -0,0 +1,285 @@
#include "ui-test-helpers.hpp"
#include "catch.hpp"
#include <QApplication>
#include <QMouseEvent>
#include <QTimer>
#include <QWidget>
#include <QWindow>
namespace advss::test {
void ProcessEvents()
{
for (int i = 0; i < 3; i++) {
QCoreApplication::processEvents();
QCoreApplication::sendPostedEvents(nullptr,
QEvent::DeferredDelete);
}
}
static void sendMouseEvent(QWidget *widget, QEvent::Type type,
const QPoint &pos, Qt::MouseButton button,
Qt::MouseButtons buttons,
Qt::KeyboardModifiers modifiers)
{
auto topLevel = widget->window();
auto window = topLevel->windowHandle();
Q_ASSERT(window);
const QPointF windowPos = widget->mapTo(topLevel, pos);
const QPointF globalPos = widget->mapToGlobal(pos);
QMouseEvent event(type, windowPos, windowPos, globalPos, button,
buttons, modifiers);
QApplication::sendEvent(window, &event);
}
void MousePress(QWidget *widget, const QPoint &pos,
Qt::KeyboardModifiers modifiers)
{
sendMouseEvent(widget, QEvent::MouseButtonPress, pos, Qt::LeftButton,
Qt::LeftButton, modifiers);
}
void MouseRelease(QWidget *widget, const QPoint &pos,
Qt::KeyboardModifiers modifiers)
{
sendMouseEvent(widget, QEvent::MouseButtonRelease, pos, Qt::LeftButton,
Qt::NoButton, modifiers);
}
void MouseMove(QWidget *widget, const QPoint &pos, Qt::MouseButtons buttons,
Qt::KeyboardModifiers modifiers)
{
sendMouseEvent(widget, QEvent::MouseMove, pos, Qt::NoButton, buttons,
modifiers);
}
void Click(QWidget *widget, const QPoint &pos, Qt::KeyboardModifiers modifiers)
{
MousePress(widget, pos, modifiers);
MouseRelease(widget, pos, modifiers);
ProcessEvents();
}
void DoubleClick(QWidget *widget, const QPoint &pos)
{
MousePress(widget, pos);
MouseRelease(widget, pos);
sendMouseEvent(widget, QEvent::MouseButtonDblClick, pos, Qt::LeftButton,
Qt::LeftButton, Qt::NoModifier);
MouseRelease(widget, pos);
ProcessEvents();
}
bool DragAndDropSupported()
{
// QOffscreenDrag::drag() ignores all drags
return QGuiApplication::platformName() != "offscreen";
}
void DragAndDrop(QWidget *widget, const QPoint &from, const QPoint &to)
{
if (!DragAndDropSupported()) {
FAIL("drag and drop is not supported by the Qt platform");
}
MousePress(widget, from);
// The first move puts the view into dragging state, the second one
// starts the drag. QDrag::exec() then runs its own event loop, so the
// rest of the gesture has to be delivered from within that loop.
MouseMove(widget, from + QPoint(0, 1), Qt::LeftButton);
QTimer::singleShot(0, widget, [widget, to]() {
MouseMove(widget, to, Qt::LeftButton);
MouseMove(widget, to, Qt::LeftButton);
MouseRelease(widget, to);
});
const int distance = QApplication::startDragDistance() + 5;
MouseMove(widget, from + QPoint(0, distance), Qt::LeftButton);
ProcessEvents();
}
ModelChecker::ModelChecker(QAbstractItemModel *model, int nameRole)
: QObject(model),
_model(model),
_nameRole(nameRole)
{
connect(model, &QAbstractItemModel::rowsAboutToBeInserted, this,
&ModelChecker::RowsAboutToBeInserted);
connect(model, &QAbstractItemModel::rowsInserted, this,
&ModelChecker::RowsInserted);
connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this,
&ModelChecker::RowsAboutToBeRemoved);
connect(model, &QAbstractItemModel::rowsRemoved, this,
&ModelChecker::RowsRemoved);
connect(model, &QAbstractItemModel::rowsAboutToBeMoved, this,
&ModelChecker::RowsAboutToBeMoved);
connect(model, &QAbstractItemModel::rowsMoved, this,
&ModelChecker::RowsMoved);
connect(model, &QAbstractItemModel::modelReset, this,
[this]() { CheckStructure(); });
connect(model, &QAbstractItemModel::layoutChanged, this,
[this]() { CheckStructure(); });
CheckStructure();
}
void ModelChecker::CheckStructure()
{
CheckStructure(QModelIndex(), 0);
}
void ModelChecker::CheckStructure(const QModelIndex &parent, int depth)
{
if (depth > 100) {
Error("model structure too deep - cycle?");
return;
}
const int count = _model->rowCount(parent);
for (int row = 0; row < count; row++) {
const auto index = _model->index(row, 0, parent);
if (!index.isValid()) {
Error(QString("invalid index at row %1 below \"%2\"")
.arg(row)
.arg(Name(parent))
.toStdString());
continue;
}
if (_model->parent(index) != parent) {
Error(QString("parent of \"%1\" does not match \"%2\"")
.arg(Name(index))
.arg(Name(parent))
.toStdString());
}
if (Name(index).isEmpty()) {
Error(QString("no name at row %1 below \"%2\"")
.arg(row)
.arg(Name(parent))
.toStdString());
}
CheckStructure(index, depth + 1);
}
}
QString ModelChecker::Name(const QModelIndex &index) const
{
if (!index.isValid()) {
return "<root>";
}
return _model->data(index, _nameRole).toString();
}
void ModelChecker::Error(const std::string &message)
{
_errors.emplace_back(message);
}
void ModelChecker::RowsAboutToBeInserted(const QModelIndex &parent, int, int)
{
_pending = {};
_pending.parent = parent;
_pending.oldCount = _model->rowCount(parent);
}
void ModelChecker::RowsInserted(const QModelIndex &parent, int first, int last)
{
const int expected = _pending.oldCount + last - first + 1;
const int actual = _model->rowCount(parent);
if (actual != expected) {
Error(QString("insert below \"%1\": expected %2 rows, got %3")
.arg(Name(parent))
.arg(expected)
.arg(actual)
.toStdString());
}
CheckStructure();
}
void ModelChecker::RowsAboutToBeRemoved(const QModelIndex &parent, int first,
int last)
{
_pending = {};
_pending.parent = parent;
_pending.oldCount = _model->rowCount(parent);
if (last >= _pending.oldCount || first < 0) {
Error(QString("remove below \"%1\": rows %2-%3 out of range %4")
.arg(Name(parent))
.arg(first)
.arg(last)
.arg(_pending.oldCount)
.toStdString());
}
}
void ModelChecker::RowsRemoved(const QModelIndex &parent, int first, int last)
{
const int expected = _pending.oldCount - (last - first + 1);
const int actual = _model->rowCount(parent);
if (actual != expected) {
Error(QString("remove below \"%1\": expected %2 rows, got %3")
.arg(Name(parent))
.arg(expected)
.arg(actual)
.toStdString());
}
CheckStructure();
}
void ModelChecker::RowsAboutToBeMoved(const QModelIndex &sourceParent,
int sourceStart, int sourceEnd,
const QModelIndex &destinationParent, int)
{
_pending = {};
_pending.parent = sourceParent;
_pending.oldCount = _model->rowCount(sourceParent);
_pending.destinationParent = destinationParent;
_pending.oldDestinationCount = _model->rowCount(destinationParent);
for (int row = sourceStart; row <= sourceEnd; row++) {
_pending.names << Name(_model->index(row, 0, sourceParent));
}
}
void ModelChecker::RowsMoved(const QModelIndex &sourceParent, int sourceStart,
int sourceEnd,
const QModelIndex &destinationParent,
int destinationRow)
{
const int count = sourceEnd - sourceStart + 1;
const bool sameParent = sourceParent == destinationParent;
int newStart = destinationRow;
if (sameParent && destinationRow > sourceEnd) {
newStart -= count;
}
QStringList names;
for (int row = newStart; row < newStart + count; row++) {
names << Name(_model->index(row, 0, destinationParent));
}
if (names != _pending.names) {
Error(QString("move to \"%1\" row %2: expected [%3], got [%4]")
.arg(Name(destinationParent))
.arg(destinationRow)
.arg(_pending.names.join(", "))
.arg(names.join(", "))
.toStdString());
}
if (!sameParent) {
const int sourceCount = _model->rowCount(sourceParent);
const int destinationCount =
_model->rowCount(destinationParent);
if (sourceCount != _pending.oldCount - count ||
destinationCount != _pending.oldDestinationCount + count) {
Error(QString("move from \"%1\" to \"%2\": row counts do not match")
.arg(Name(sourceParent))
.arg(Name(destinationParent))
.toStdString());
}
} else if (_model->rowCount(sourceParent) != _pending.oldCount) {
Error(QString("move within \"%1\": row count changed")
.arg(Name(sourceParent))
.toStdString());
}
CheckStructure();
}
} // namespace advss::test

76
tests/ui-test-helpers.hpp Normal file
View File

@@ -0,0 +1,76 @@
#pragma once
#include <QAbstractItemModel>
#include <QObject>
#include <QPersistentModelIndex>
#include <QPoint>
#include <QStringList>
#include <string>
#include <vector>
class QWidget;
namespace advss::test {
void ProcessEvents();
// Mouse input is sent to the native window so that Qt performs the same hit
// testing, implicit grabbing and drag and drop handling as for real input
void MousePress(QWidget *widget, const QPoint &pos,
Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void MouseRelease(QWidget *widget, const QPoint &pos,
Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void MouseMove(QWidget *widget, const QPoint &pos,
Qt::MouseButtons buttons = Qt::NoButton,
Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void Click(QWidget *widget, const QPoint &pos,
Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void DoubleClick(QWidget *widget, const QPoint &pos);
bool DragAndDropSupported();
void DragAndDrop(QWidget *widget, const QPoint &from, const QPoint &to);
// Validates the rows*() signals and the parent/child structure of a model and
// records every inconsistency it detects
class ModelChecker : public QObject {
Q_OBJECT
public:
ModelChecker(QAbstractItemModel *model, int nameRole);
const std::vector<std::string> &Errors() const { return _errors; }
void CheckStructure();
private slots:
void RowsAboutToBeInserted(const QModelIndex &parent, int first,
int last);
void RowsInserted(const QModelIndex &parent, int first, int last);
void RowsAboutToBeRemoved(const QModelIndex &parent, int first,
int last);
void RowsRemoved(const QModelIndex &parent, int first, int last);
void RowsAboutToBeMoved(const QModelIndex &sourceParent,
int sourceStart, int sourceEnd,
const QModelIndex &destinationParent,
int destinationRow);
void RowsMoved(const QModelIndex &sourceParent, int sourceStart,
int sourceEnd, const QModelIndex &destinationParent,
int destinationRow);
private:
void CheckStructure(const QModelIndex &parent, int depth);
QString Name(const QModelIndex &index) const;
void Error(const std::string &message);
QAbstractItemModel *_model;
int _nameRole;
struct Change {
QPersistentModelIndex parent;
int oldCount = 0;
QPersistentModelIndex destinationParent;
int oldDestinationCount = 0;
QStringList names;
};
Change _pending;
std::vector<std::string> _errors;
};
} // namespace advss::test