mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-10-25 18:09:45 -05:00
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:
@@ -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.
|
||||
|
||||
8
tests/stubs/macro-undo-redo.cpp
Normal file
8
tests/stubs/macro-undo-redo.cpp
Normal 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
1038
tests/test-macro-tree.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
285
tests/ui-test-helpers.cpp
Normal 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
76
tests/ui-test-helpers.hpp
Normal 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
|
||||
Reference in New Issue
Block a user