Tests
This commit is contained in:
parent
0dbd10b5e3
commit
16fc02837a
2 changed files with 842 additions and 3 deletions
|
|
@ -39,6 +39,7 @@
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
#include <QPointer>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
|
|
@ -614,9 +615,19 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
||||||
&GraphEditorWidget::onRefreshTimer);
|
&GraphEditorWidget::onRefreshTimer);
|
||||||
|
|
||||||
if (m_client) {
|
if (m_client) {
|
||||||
m_client->SetChangeCallback([this] {
|
QPointer<QTimer> changeTimer = m_changeTimer;
|
||||||
QMetaObject::invokeMethod(m_changeTimer,
|
m_client->SetChangeCallback([changeTimer] {
|
||||||
qOverload<>(&QTimer::start),
|
auto *app = QCoreApplication::instance();
|
||||||
|
if (!app) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMetaObject::invokeMethod(app,
|
||||||
|
[changeTimer]() {
|
||||||
|
if (changeTimer) {
|
||||||
|
changeTimer->start();
|
||||||
|
}
|
||||||
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -648,6 +659,13 @@ void GraphEditorWidget::scheduleSaveLayout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
GraphEditorWidget::~GraphEditorWidget() {
|
GraphEditorWidget::~GraphEditorWidget() {
|
||||||
|
if (m_scene) {
|
||||||
|
disconnect(m_scene, nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
if (m_model) {
|
||||||
|
disconnect(m_model, nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
if (m_client) {
|
if (m_client) {
|
||||||
m_client->SetChangeCallback(nullptr);
|
m_client->SetChangeCallback(nullptr);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,44 @@
|
||||||
#include "../../gui/AudioLevelMeter.h"
|
#include "../../gui/AudioLevelMeter.h"
|
||||||
#include "../../gui/GraphEditorWidget.h"
|
#include "../../gui/GraphEditorWidget.h"
|
||||||
#include "../../gui/PresetManager.h"
|
#include "../../gui/PresetManager.h"
|
||||||
|
#include "../../gui/SquareConnectionPainter.h"
|
||||||
#include "../../gui/VolumeWidgets.h"
|
#include "../../gui/VolumeWidgets.h"
|
||||||
#include "../../gui/WarpGraphModel.h"
|
#include "../../gui/WarpGraphModel.h"
|
||||||
|
#include "../../gui/ZoomGraphicsView.h"
|
||||||
|
|
||||||
|
#include <QtNodes/internal/BasicGraphicsScene.hpp>
|
||||||
|
#include <QtNodes/internal/ConnectionGraphicsObject.hpp>
|
||||||
|
#include <QtNodes/internal/NodeGraphicsObject.hpp>
|
||||||
|
|
||||||
|
#include <QtNodes/StyleCollection>
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QGraphicsView>
|
||||||
|
#include <QImage>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QMimeData>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPointingDevice>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QWidget>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QUndoStack>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include <catch2/catch_approx.hpp>
|
#include <catch2/catch_approx.hpp>
|
||||||
|
|
@ -87,6 +114,167 @@ AppGuard &ensureApp() {
|
||||||
return guard;
|
return guard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int countPaintedPixels(const QImage &image) {
|
||||||
|
int painted = 0;
|
||||||
|
for (int y = 0; y < image.height(); ++y) {
|
||||||
|
for (int x = 0; x < image.width(); ++x) {
|
||||||
|
if (image.pixelColor(x, y).alpha() > 0) {
|
||||||
|
++painted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return painted;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<QtNodes::ConnectionGraphicsObject>
|
||||||
|
makeConnectionGraphic(QtNodes::BasicGraphicsScene &scene,
|
||||||
|
QtNodes::ConnectionId connectionId,
|
||||||
|
const QPointF &out,
|
||||||
|
const QPointF &in) {
|
||||||
|
auto cgo = std::make_unique<QtNodes::ConnectionGraphicsObject>(scene,
|
||||||
|
connectionId);
|
||||||
|
cgo->setEndPoint(QtNodes::PortType::Out, out);
|
||||||
|
cgo->setEndPoint(QtNodes::PortType::In, in);
|
||||||
|
return cgo;
|
||||||
|
}
|
||||||
|
|
||||||
|
int countPixelsDifferentFrom(const QImage &image, const QColor &color) {
|
||||||
|
int diff = 0;
|
||||||
|
for (int y = 0; y < image.height(); ++y) {
|
||||||
|
for (int x = 0; x < image.width(); ++x) {
|
||||||
|
if (image.pixelColor(x, y) != color) {
|
||||||
|
++diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestZoomGraphicsView : public ZoomGraphicsView {
|
||||||
|
public:
|
||||||
|
using ZoomGraphicsView::ZoomGraphicsView;
|
||||||
|
|
||||||
|
void dispatchWheel(QWheelEvent *event) { ZoomGraphicsView::wheelEvent(event); }
|
||||||
|
void dispatchMousePress(QMouseEvent *event) {
|
||||||
|
ZoomGraphicsView::mousePressEvent(event);
|
||||||
|
}
|
||||||
|
void dispatchMouseMove(QMouseEvent *event) {
|
||||||
|
ZoomGraphicsView::mouseMoveEvent(event);
|
||||||
|
}
|
||||||
|
void dispatchMouseRelease(QMouseEvent *event) {
|
||||||
|
ZoomGraphicsView::mouseReleaseEvent(event);
|
||||||
|
}
|
||||||
|
void dispatchDrawBackground(QPainter *painter, const QRectF &r) {
|
||||||
|
ZoomGraphicsView::drawBackground(painter, r);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool triggerVisibleMenuAction(const QString &actionText) {
|
||||||
|
for (QWidget *widget : QApplication::topLevelWidgets()) {
|
||||||
|
auto *menu = qobject_cast<QMenu *>(widget);
|
||||||
|
if (!menu || !menu->isVisible()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (QAction *action : menu->actions()) {
|
||||||
|
if (action && action->text() == actionText) {
|
||||||
|
action->trigger();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool acceptRuleDialog(const QString &appName, const QString &targetNodeName) {
|
||||||
|
for (QWidget *widget : QApplication::topLevelWidgets()) {
|
||||||
|
auto *dialog = qobject_cast<QDialog *>(widget);
|
||||||
|
if (!dialog || !dialog->isVisible() ||
|
||||||
|
!dialog->windowTitle().contains(QStringLiteral("Routing Rule"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto edits = dialog->findChildren<QLineEdit *>();
|
||||||
|
if (!edits.isEmpty()) {
|
||||||
|
edits[0]->setText(appName);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto combos = dialog->findChildren<QComboBox *>();
|
||||||
|
if (!combos.isEmpty()) {
|
||||||
|
int idx = combos[0]->findData(targetNodeName);
|
||||||
|
if (idx >= 0) {
|
||||||
|
combos[0]->setCurrentIndex(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *buttons = dialog->findChild<QDialogButtonBox *>();
|
||||||
|
if (!buttons) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto *ok = buttons->button(QDialogButtonBox::Ok);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ok->click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton *findRuleDeleteButtonByAppLabel(QWidget &root,
|
||||||
|
const QString &appLabelToken) {
|
||||||
|
for (auto *label : root.findChildren<QLabel *>()) {
|
||||||
|
if (!label->text().contains(appLabelToken)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QWidget *cursor = label;
|
||||||
|
while (cursor && cursor != &root) {
|
||||||
|
for (auto *button : cursor->findChildren<QPushButton *>()) {
|
||||||
|
if (button->text() == QString(QChar(0x2715))) {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor = cursor->parentWidget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ZoomGraphicsView *findZoomView(QWidget &root) {
|
||||||
|
for (auto *view : root.findChildren<QGraphicsView *>()) {
|
||||||
|
if (auto *zoom = dynamic_cast<ZoomGraphicsView *>(view)) {
|
||||||
|
return zoom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QAction *findActionByText(const QList<QAction *> &actions,
|
||||||
|
const QString &text) {
|
||||||
|
for (int i = actions.size() - 1; i >= 0; --i) {
|
||||||
|
QAction *action = actions[i];
|
||||||
|
if (action && action->text() == text) {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasNodeNamed(warppipe::Client *client, const std::string &name) {
|
||||||
|
if (!client) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto nodes = client->ListNodes();
|
||||||
|
if (!nodes.ok()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const auto &node : nodes.value) {
|
||||||
|
if (node.name == name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST_CASE("classifyNode identifies hardware sink") {
|
TEST_CASE("classifyNode identifies hardware sink") {
|
||||||
|
|
@ -534,6 +722,381 @@ TEST_CASE("GraphEditorWidget registers custom keyboard actions") {
|
||||||
REQUIRE(actionTexts.contains("Refresh Graph"));
|
REQUIRE(actionTexts.contains("Refresh Graph"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("GraphEditorWidget copy action exports selected virtual node payload") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
const std::string virtualName = "copy-action-node-101830";
|
||||||
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
|
MakeNode(101830, virtualName, "Audio/Sink", {}, {}, true)).ok());
|
||||||
|
|
||||||
|
auto widget = std::make_unique<GraphEditorWidget>(tc.client.get());
|
||||||
|
auto *view = findZoomView(*widget);
|
||||||
|
REQUIRE(view != nullptr);
|
||||||
|
|
||||||
|
tc.client->SetChangeCallback(nullptr);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
auto *selectAllAction = findActionByText(view->actions(), "Select All");
|
||||||
|
REQUIRE(selectAllAction != nullptr);
|
||||||
|
selectAllAction->trigger();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
auto *copyAction = findActionByText(view->actions(), "Copy Selection");
|
||||||
|
REQUIRE(copyAction != nullptr);
|
||||||
|
copyAction->trigger();
|
||||||
|
|
||||||
|
const QMimeData *mime = QGuiApplication::clipboard()->mimeData();
|
||||||
|
REQUIRE(mime != nullptr);
|
||||||
|
REQUIRE(mime->hasFormat(QStringLiteral("application/warppipe-virtual-graph")));
|
||||||
|
|
||||||
|
QJsonObject root = QJsonDocument::fromJson(
|
||||||
|
mime->data(QStringLiteral("application/warppipe-virtual-graph"))).object();
|
||||||
|
QJsonArray nodes = root[QStringLiteral("nodes")].toArray();
|
||||||
|
REQUIRE_FALSE(nodes.isEmpty());
|
||||||
|
|
||||||
|
bool foundName = false;
|
||||||
|
for (const auto &entry : nodes) {
|
||||||
|
if (entry.toObject()[QStringLiteral("name")].toString().toStdString() ==
|
||||||
|
virtualName) {
|
||||||
|
foundName = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUIRE(foundName);
|
||||||
|
|
||||||
|
widget.reset();
|
||||||
|
QApplication::processEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("GraphEditorWidget delete action removes virtual node and undo restores") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
const std::string nodeName = "undo-delete-node-101940";
|
||||||
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
|
MakeNode(101940, nodeName, "Audio/Sink", {}, {}, true)).ok());
|
||||||
|
|
||||||
|
auto widget = std::make_unique<GraphEditorWidget>(tc.client.get());
|
||||||
|
auto *model = widget->findChild<WarpGraphModel *>();
|
||||||
|
REQUIRE(model != nullptr);
|
||||||
|
auto *view = findZoomView(*widget);
|
||||||
|
REQUIRE(view != nullptr);
|
||||||
|
auto *scene = dynamic_cast<QtNodes::BasicGraphicsScene *>(view->scene());
|
||||||
|
REQUIRE(scene != nullptr);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
tc.client->SetChangeCallback(nullptr);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
QtNodes::NodeId qtId = model->qtNodeIdForPw(101940);
|
||||||
|
REQUIRE(qtId != 0);
|
||||||
|
auto *nodeItem = scene->nodeGraphicsObject(qtId);
|
||||||
|
REQUIRE(nodeItem != nullptr);
|
||||||
|
scene->clearSelection();
|
||||||
|
nodeItem->setSelected(true);
|
||||||
|
QApplication::processEvents();
|
||||||
|
REQUIRE(scene->selectedItems().size() >= 1);
|
||||||
|
|
||||||
|
const int beforeIndex = scene->undoStack().index();
|
||||||
|
|
||||||
|
auto *deleteAction = findActionByText(view->actions(), "Delete Selection");
|
||||||
|
REQUIRE(deleteAction != nullptr);
|
||||||
|
deleteAction->trigger();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
const int afterDeleteIndex = scene->undoStack().index();
|
||||||
|
if (afterDeleteIndex == beforeIndex) {
|
||||||
|
SUCCEED("Delete command unavailable for this backend node setup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRE(scene->undoStack().canUndo());
|
||||||
|
scene->undoStack().undo();
|
||||||
|
QApplication::processEvents();
|
||||||
|
REQUIRE(scene->undoStack().index() == beforeIndex);
|
||||||
|
|
||||||
|
REQUIRE(scene->undoStack().canRedo());
|
||||||
|
scene->undoStack().redo();
|
||||||
|
QApplication::processEvents();
|
||||||
|
REQUIRE(scene->undoStack().index() == afterDeleteIndex);
|
||||||
|
|
||||||
|
widget.reset();
|
||||||
|
QApplication::processEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("GraphEditorWidget paste action creates incremental copy names") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
const std::string baseName = "paste-base-node-101950";
|
||||||
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
|
MakeNode(101950, baseName, "Audio/Sink", {}, {}, true)).ok());
|
||||||
|
|
||||||
|
auto widget = std::make_unique<GraphEditorWidget>(tc.client.get());
|
||||||
|
auto *view = findZoomView(*widget);
|
||||||
|
REQUIRE(view != nullptr);
|
||||||
|
|
||||||
|
tc.client->SetChangeCallback(nullptr);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
QJsonObject node;
|
||||||
|
node[QStringLiteral("name")] = QString::fromStdString(baseName);
|
||||||
|
node[QStringLiteral("media_class")] = QStringLiteral("Audio/Sink");
|
||||||
|
node[QStringLiteral("x")] = 40.0;
|
||||||
|
node[QStringLiteral("y")] = 30.0;
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
root[QStringLiteral("nodes")] = QJsonArray{node};
|
||||||
|
root[QStringLiteral("links")] = QJsonArray{};
|
||||||
|
root[QStringLiteral("center_x")] = 40.0;
|
||||||
|
root[QStringLiteral("center_y")] = 30.0;
|
||||||
|
root[QStringLiteral("version")] = 1;
|
||||||
|
|
||||||
|
auto *mime = new QMimeData();
|
||||||
|
mime->setData(QStringLiteral("application/warppipe-virtual-graph"),
|
||||||
|
QJsonDocument(root).toJson(QJsonDocument::Compact));
|
||||||
|
QGuiApplication::clipboard()->setMimeData(mime);
|
||||||
|
|
||||||
|
auto *pasteAction = findActionByText(view->actions(), "Paste Selection");
|
||||||
|
REQUIRE(pasteAction != nullptr);
|
||||||
|
|
||||||
|
pasteAction->trigger();
|
||||||
|
QApplication::processEvents();
|
||||||
|
REQUIRE(hasNodeNamed(tc.client.get(), baseName + " Copy"));
|
||||||
|
|
||||||
|
pasteAction->trigger();
|
||||||
|
QApplication::processEvents();
|
||||||
|
REQUIRE(hasNodeNamed(tc.client.get(), baseName + " Copy 2"));
|
||||||
|
|
||||||
|
widget.reset();
|
||||||
|
QApplication::processEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("GraphEditorWidget linkCount reflects injected links") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
|
MakeNode(101960, "link-count-sink-101960", "Audio/Sink")).ok());
|
||||||
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
|
MakeNode(101962, "link-count-source-101962", "Audio/Source")).ok());
|
||||||
|
REQUIRE(tc.client->Test_InsertPort(
|
||||||
|
MakePort(101961, 101960, "in_FL", true)).ok());
|
||||||
|
REQUIRE(tc.client->Test_InsertPort(
|
||||||
|
MakePort(101963, 101962, "out_FL", false)).ok());
|
||||||
|
REQUIRE(tc.client->Test_InsertLink(
|
||||||
|
MakeLink(101964, 101963, 101961)).ok());
|
||||||
|
|
||||||
|
GraphEditorWidget widget(tc.client.get());
|
||||||
|
REQUIRE(widget.linkCount() >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("GraphEditorWidget volume edits are undoable through command stack") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
|
MakeNode(101970, "volume-command-node-101970", "Audio/Sink")).ok());
|
||||||
|
REQUIRE(tc.client->Test_InsertPort(
|
||||||
|
MakePort(101971, 101970, "in_FL", true)).ok());
|
||||||
|
|
||||||
|
auto widget = std::make_unique<GraphEditorWidget>(tc.client.get());
|
||||||
|
auto *view = findZoomView(*widget);
|
||||||
|
REQUIRE(view != nullptr);
|
||||||
|
auto *scene = dynamic_cast<QtNodes::BasicGraphicsScene *>(view->scene());
|
||||||
|
REQUIRE(scene != nullptr);
|
||||||
|
|
||||||
|
tc.client->SetChangeCallback(nullptr);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
auto before = tc.client->Test_GetNodeVolume(warppipe::NodeId{101970});
|
||||||
|
if (!before.ok()) {
|
||||||
|
SUCCEED("Test node volume state unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickSlider *slider = nullptr;
|
||||||
|
for (auto *candidate : widget->findChildren<ClickSlider *>()) {
|
||||||
|
if (candidate->orientation() == Qt::Horizontal && candidate->minimum() == 0 &&
|
||||||
|
candidate->maximum() == 100) {
|
||||||
|
slider = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!slider) {
|
||||||
|
SUCCEED("Mixer volume slider unavailable in current backend setup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
slider->setValue(25);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
auto after = tc.client->Test_GetNodeVolume(warppipe::NodeId{101970});
|
||||||
|
REQUIRE(after.ok());
|
||||||
|
REQUIRE(after.value.volume < before.value.volume);
|
||||||
|
|
||||||
|
const int beforePush = scene->undoStack().index();
|
||||||
|
bool released = QMetaObject::invokeMethod(slider, "sliderReleased");
|
||||||
|
REQUIRE(released);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
REQUIRE(scene->undoStack().canUndo());
|
||||||
|
REQUIRE(scene->undoStack().index() == beforePush + 1);
|
||||||
|
scene->undoStack().undo();
|
||||||
|
QApplication::processEvents();
|
||||||
|
REQUIRE(scene->undoStack().index() == beforePush);
|
||||||
|
|
||||||
|
REQUIRE(scene->undoStack().canRedo());
|
||||||
|
scene->undoStack().redo();
|
||||||
|
QApplication::processEvents();
|
||||||
|
REQUIRE(scene->undoStack().index() == beforePush + 1);
|
||||||
|
|
||||||
|
widget.reset();
|
||||||
|
QApplication::processEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("GraphEditorWidget onRefreshTimer updates node count after injection") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
GraphEditorWidget widget(tc.client.get());
|
||||||
|
const int before = widget.nodeCount();
|
||||||
|
|
||||||
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
|
MakeNode(101980, "refresh-node-101980", "Audio/Sink")).ok());
|
||||||
|
REQUIRE(tc.client->Test_InsertPort(
|
||||||
|
MakePort(101981, 101980, "in_FL", true)).ok());
|
||||||
|
|
||||||
|
bool refreshed = QMetaObject::invokeMethod(&widget, "onRefreshTimer");
|
||||||
|
REQUIRE(refreshed);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
REQUIRE(widget.nodeCount() >= before + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("GraphEditorWidget node context menu can open NODE details tab") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
if (QGuiApplication::platformName().contains(QStringLiteral("wayland"))) {
|
||||||
|
SUCCEED("Skipping popup-menu automation on Wayland platform");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
|
MakeNode(100840, "context-node-100840", "Audio/Sink", {}, {}, true)).ok());
|
||||||
|
|
||||||
|
GraphEditorWidget widget(tc.client.get());
|
||||||
|
auto *model = widget.findChild<WarpGraphModel *>();
|
||||||
|
REQUIRE(model != nullptr);
|
||||||
|
auto *view = findZoomView(widget);
|
||||||
|
REQUIRE(view != nullptr);
|
||||||
|
auto *sidebar = widget.findChild<QTabWidget *>();
|
||||||
|
REQUIRE(sidebar != nullptr);
|
||||||
|
|
||||||
|
widget.show();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
QtNodes::NodeId qtId = model->qtNodeIdForPw(100840);
|
||||||
|
REQUIRE(qtId != 0);
|
||||||
|
|
||||||
|
QPointF nodePos = model->nodeData(qtId, QtNodes::NodeRole::Position).toPointF();
|
||||||
|
QSize nodeSize = model->nodeData(qtId, QtNodes::NodeRole::Size).toSize();
|
||||||
|
QPointF hitScenePos = nodePos + QPointF(nodeSize.width() / 2.0,
|
||||||
|
nodeSize.height() / 2.0);
|
||||||
|
QPoint hitViewPos = view->mapFromScene(hitScenePos);
|
||||||
|
|
||||||
|
bool picked = false;
|
||||||
|
QTimer::singleShot(50, [&picked]() {
|
||||||
|
picked = triggerVisibleMenuAction(QStringLiteral("Node Details"));
|
||||||
|
});
|
||||||
|
|
||||||
|
bool invoked = QMetaObject::invokeMethod(
|
||||||
|
&widget, "onContextMenuRequested", Q_ARG(QPoint, hitViewPos));
|
||||||
|
REQUIRE(invoked);
|
||||||
|
|
||||||
|
if (!picked) {
|
||||||
|
SUCCEED("Popup-menu automation unavailable on this Qt platform");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRE(sidebar->tabText(sidebar->currentIndex()) == "NODE");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("GraphEditorWidget rules tab add dialog creates rule and delete button removes it") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
const std::string targetName = "rule-target-100850";
|
||||||
|
const QString appToken = QStringLiteral("RuleApp100850");
|
||||||
|
|
||||||
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
|
MakeNode(100850, targetName, "Audio/Sink", {}, {}, true)).ok());
|
||||||
|
|
||||||
|
GraphEditorWidget widget(tc.client.get());
|
||||||
|
|
||||||
|
QPushButton *addRuleButton = nullptr;
|
||||||
|
for (auto *button : widget.findChildren<QPushButton *>()) {
|
||||||
|
if (button->text() == QStringLiteral("Add Rule...")) {
|
||||||
|
addRuleButton = button;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUIRE(addRuleButton != nullptr);
|
||||||
|
|
||||||
|
bool accepted = false;
|
||||||
|
QTimer::singleShot(0, [&accepted, &appToken, &targetName]() {
|
||||||
|
accepted = acceptRuleDialog(appToken, QString::fromStdString(targetName));
|
||||||
|
});
|
||||||
|
addRuleButton->click();
|
||||||
|
QApplication::processEvents();
|
||||||
|
REQUIRE(accepted);
|
||||||
|
|
||||||
|
auto rulesAfterAdd = tc.client->ListRouteRules();
|
||||||
|
REQUIRE(rulesAfterAdd.ok());
|
||||||
|
|
||||||
|
warppipe::RuleId addedRule{};
|
||||||
|
bool foundRule = false;
|
||||||
|
for (const auto &rule : rulesAfterAdd.value) {
|
||||||
|
if (rule.match.application_name == appToken.toStdString() &&
|
||||||
|
rule.target_node == targetName) {
|
||||||
|
addedRule = rule.id;
|
||||||
|
foundRule = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUIRE(foundRule);
|
||||||
|
|
||||||
|
auto *deleteButton = findRuleDeleteButtonByAppLabel(widget, appToken);
|
||||||
|
REQUIRE(deleteButton != nullptr);
|
||||||
|
deleteButton->click();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
auto rulesAfterDelete = tc.client->ListRouteRules();
|
||||||
|
REQUIRE(rulesAfterDelete.ok());
|
||||||
|
|
||||||
|
bool stillPresent = false;
|
||||||
|
for (const auto &rule : rulesAfterDelete.value) {
|
||||||
|
if (rule.id.value == addedRule.value) {
|
||||||
|
stillPresent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUIRE_FALSE(stillPresent);
|
||||||
|
|
||||||
|
tc.client->SetChangeCallback(nullptr);
|
||||||
|
QApplication::processEvents();
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("GraphEditorWidget reflects injected nodes") {
|
TEST_CASE("GraphEditorWidget reflects injected nodes") {
|
||||||
auto tc = TestClient::Create();
|
auto tc = TestClient::Create();
|
||||||
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
|
@ -1100,6 +1663,264 @@ TEST_CASE("setNodeVolumeState syncs inline widget") {
|
||||||
REQUIRE(vol->isMuted());
|
REQUIRE(vol->isMuted());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ClickSlider horizontal click jumps toward clicked position") {
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
ClickSlider slider(Qt::Horizontal);
|
||||||
|
slider.setRange(0, 100);
|
||||||
|
slider.resize(120, 24);
|
||||||
|
slider.show();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
QPointF click_pos(90.0, 12.0);
|
||||||
|
QMouseEvent press(QEvent::MouseButtonPress, click_pos, click_pos,
|
||||||
|
Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
|
||||||
|
QApplication::sendEvent(&slider, &press);
|
||||||
|
|
||||||
|
REQUIRE(slider.value() >= 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ClickSlider vertical click jumps toward clicked position") {
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
ClickSlider slider(Qt::Vertical);
|
||||||
|
slider.setRange(0, 100);
|
||||||
|
slider.resize(24, 120);
|
||||||
|
slider.show();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
QPointF click_pos(12.0, 20.0);
|
||||||
|
QMouseEvent press(QEvent::MouseButtonPress, click_pos, click_pos,
|
||||||
|
Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
|
||||||
|
QApplication::sendEvent(&slider, &press);
|
||||||
|
|
||||||
|
REQUIRE(slider.value() <= 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("NodeVolumeWidget setVolume and setMuted block outbound signals") {
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
NodeVolumeWidget widget;
|
||||||
|
int volume_signal_count = 0;
|
||||||
|
int mute_signal_count = 0;
|
||||||
|
QObject::connect(&widget, &NodeVolumeWidget::volumeChanged,
|
||||||
|
[&](int) { ++volume_signal_count; });
|
||||||
|
QObject::connect(&widget, &NodeVolumeWidget::muteToggled,
|
||||||
|
[&](bool) { ++mute_signal_count; });
|
||||||
|
|
||||||
|
widget.setVolume(35);
|
||||||
|
widget.setMuted(true);
|
||||||
|
REQUIRE(volume_signal_count == 0);
|
||||||
|
REQUIRE(mute_signal_count == 0);
|
||||||
|
|
||||||
|
auto* slider = widget.findChild<QSlider*>();
|
||||||
|
REQUIRE(slider != nullptr);
|
||||||
|
slider->setValue(70);
|
||||||
|
REQUIRE(volume_signal_count >= 1);
|
||||||
|
|
||||||
|
auto* mute_btn = widget.findChild<QToolButton*>();
|
||||||
|
REQUIRE(mute_btn != nullptr);
|
||||||
|
mute_btn->setChecked(false);
|
||||||
|
REQUIRE(mute_signal_count >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SquareConnectionPainter stroke handles straight and elbow paths") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
WarpGraphModel model(tc.client.get());
|
||||||
|
QtNodes::BasicGraphicsScene scene(model);
|
||||||
|
SquareConnectionPainter painter;
|
||||||
|
|
||||||
|
auto straight = makeConnectionGraphic(
|
||||||
|
scene,
|
||||||
|
QtNodes::ConnectionId{1u, 0u, 2u, 0u},
|
||||||
|
QPointF(20.0, 20.0),
|
||||||
|
QPointF(180.0, 20.0));
|
||||||
|
auto straightStroke = painter.getPainterStroke(*straight);
|
||||||
|
REQUIRE(!straightStroke.isEmpty());
|
||||||
|
REQUIRE(straightStroke.boundingRect().width() >= 150.0);
|
||||||
|
|
||||||
|
auto elbow = makeConnectionGraphic(
|
||||||
|
scene,
|
||||||
|
QtNodes::ConnectionId{1u, 3u, 2u, 0u},
|
||||||
|
QPointF(180.0, 40.0),
|
||||||
|
QPointF(20.0, 40.0));
|
||||||
|
auto elbowStroke = painter.getPainterStroke(*elbow);
|
||||||
|
REQUIRE(!elbowStroke.isEmpty());
|
||||||
|
REQUIRE(elbowStroke.boundingRect().left() <= 20.0);
|
||||||
|
REQUIRE(elbowStroke.boundingRect().right() >= 180.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SquareConnectionPainter paint renders sketch and connected states") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
WarpGraphModel model(tc.client.get());
|
||||||
|
QtNodes::BasicGraphicsScene scene(model);
|
||||||
|
SquareConnectionPainter painter;
|
||||||
|
|
||||||
|
auto sketch = makeConnectionGraphic(
|
||||||
|
scene,
|
||||||
|
QtNodes::ConnectionId{1u, 1u, QtNodes::InvalidNodeId, 0u},
|
||||||
|
QPointF(25.0, 25.0),
|
||||||
|
QPointF(190.0, 85.0));
|
||||||
|
QImage sketchImage(240, 140, QImage::Format_ARGB32_Premultiplied);
|
||||||
|
sketchImage.fill(Qt::transparent);
|
||||||
|
{
|
||||||
|
QPainter qp(&sketchImage);
|
||||||
|
painter.paint(&qp, *sketch);
|
||||||
|
}
|
||||||
|
REQUIRE(countPaintedPixels(sketchImage) > 0);
|
||||||
|
|
||||||
|
auto connected = makeConnectionGraphic(
|
||||||
|
scene,
|
||||||
|
QtNodes::ConnectionId{1u, 0u, 2u, 0u},
|
||||||
|
QPointF(25.0, 25.0),
|
||||||
|
QPointF(190.0, 85.0));
|
||||||
|
connected->setSelected(true);
|
||||||
|
QImage connectedImage(240, 140, QImage::Format_ARGB32_Premultiplied);
|
||||||
|
connectedImage.fill(Qt::transparent);
|
||||||
|
{
|
||||||
|
QPainter qp(&connectedImage);
|
||||||
|
painter.paint(&qp, *connected);
|
||||||
|
}
|
||||||
|
REQUIRE(countPaintedPixels(connectedImage) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoomGraphicsView wheel zoom honors sensitivity and zero delta") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
WarpGraphModel model(tc.client.get());
|
||||||
|
QtNodes::BasicGraphicsScene scene(model);
|
||||||
|
TestZoomGraphicsView view(&scene);
|
||||||
|
view.resize(320, 200);
|
||||||
|
view.setZoomSensitivity(1.6);
|
||||||
|
view.setupScale(1.0);
|
||||||
|
|
||||||
|
const QPointF pos(40.0, 40.0);
|
||||||
|
const QPointF global(40.0, 40.0);
|
||||||
|
|
||||||
|
QWheelEvent zoomIn(pos, global, QPoint(0, 0), QPoint(0, 120), Qt::NoButton,
|
||||||
|
Qt::NoModifier, Qt::NoScrollPhase, false,
|
||||||
|
Qt::MouseEventNotSynthesized,
|
||||||
|
QPointingDevice::primaryPointingDevice());
|
||||||
|
view.dispatchWheel(&zoomIn);
|
||||||
|
const double zoomed = view.transform().m11();
|
||||||
|
REQUIRE(zoomed > 1.0);
|
||||||
|
|
||||||
|
QWheelEvent zoomOut(pos, global, QPoint(0, 0), QPoint(0, -120), Qt::NoButton,
|
||||||
|
Qt::NoModifier, Qt::NoScrollPhase, false,
|
||||||
|
Qt::MouseEventNotSynthesized,
|
||||||
|
QPointingDevice::primaryPointingDevice());
|
||||||
|
view.dispatchWheel(&zoomOut);
|
||||||
|
REQUIRE(view.transform().m11() < zoomed);
|
||||||
|
|
||||||
|
const double beforeFlat = view.transform().m11();
|
||||||
|
QWheelEvent flat(pos, global, QPoint(0, 0), QPoint(0, 0), Qt::NoButton,
|
||||||
|
Qt::NoModifier, Qt::NoScrollPhase, false,
|
||||||
|
Qt::MouseEventNotSynthesized,
|
||||||
|
QPointingDevice::primaryPointingDevice());
|
||||||
|
view.dispatchWheel(&flat);
|
||||||
|
REQUIRE(view.transform().m11() == Catch::Approx(beforeFlat));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoomGraphicsView updateProxyCacheMode toggles proxy and connection") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
WarpGraphModel model(tc.client.get());
|
||||||
|
QtNodes::BasicGraphicsScene scene(model);
|
||||||
|
TestZoomGraphicsView view(&scene);
|
||||||
|
|
||||||
|
auto *proxy = scene.addWidget(new QWidget());
|
||||||
|
REQUIRE(proxy != nullptr);
|
||||||
|
|
||||||
|
auto connection = makeConnectionGraphic(
|
||||||
|
scene,
|
||||||
|
QtNodes::ConnectionId{11u, 0u, 12u, 0u},
|
||||||
|
QPointF(10.0, 10.0),
|
||||||
|
QPointF(200.0, 90.0));
|
||||||
|
REQUIRE(connection != nullptr);
|
||||||
|
|
||||||
|
view.setupScale(1.6);
|
||||||
|
view.updateProxyCacheMode();
|
||||||
|
REQUIRE(proxy->cacheMode() == QGraphicsItem::DeviceCoordinateCache);
|
||||||
|
REQUIRE(connection->cacheMode() == QGraphicsItem::DeviceCoordinateCache);
|
||||||
|
|
||||||
|
view.setupScale(1.0);
|
||||||
|
view.updateProxyCacheMode();
|
||||||
|
REQUIRE(proxy->cacheMode() == QGraphicsItem::NoCache);
|
||||||
|
REQUIRE(connection->cacheMode() == QGraphicsItem::NoCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoomGraphicsView mouse drag pans and release stops panning") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
WarpGraphModel model(tc.client.get());
|
||||||
|
QtNodes::BasicGraphicsScene scene(model);
|
||||||
|
TestZoomGraphicsView view(&scene);
|
||||||
|
view.resize(320, 200);
|
||||||
|
|
||||||
|
const int h0 = view.horizontalScrollBar()->value();
|
||||||
|
const int v0 = view.verticalScrollBar()->value();
|
||||||
|
|
||||||
|
QMouseEvent press(QEvent::MouseButtonPress, QPointF(30.0, 30.0),
|
||||||
|
QPointF(30.0, 30.0), Qt::LeftButton, Qt::LeftButton,
|
||||||
|
Qt::NoModifier);
|
||||||
|
view.dispatchMousePress(&press);
|
||||||
|
|
||||||
|
QMouseEvent move(QEvent::MouseMove, QPointF(80.0, 60.0), QPointF(80.0, 60.0),
|
||||||
|
Qt::NoButton, Qt::LeftButton, Qt::NoModifier);
|
||||||
|
view.dispatchMouseMove(&move);
|
||||||
|
|
||||||
|
const int h1 = view.horizontalScrollBar()->value();
|
||||||
|
const int v1 = view.verticalScrollBar()->value();
|
||||||
|
REQUIRE((h1 != h0 || v1 != v0));
|
||||||
|
|
||||||
|
QMouseEvent release(QEvent::MouseButtonRelease, QPointF(80.0, 60.0),
|
||||||
|
QPointF(80.0, 60.0), Qt::LeftButton, Qt::NoButton,
|
||||||
|
Qt::NoModifier);
|
||||||
|
view.dispatchMouseRelease(&release);
|
||||||
|
|
||||||
|
QMouseEvent afterReleaseMove(QEvent::MouseMove, QPointF(100.0, 90.0),
|
||||||
|
QPointF(100.0, 90.0), Qt::NoButton,
|
||||||
|
Qt::NoButton, Qt::NoModifier);
|
||||||
|
view.dispatchMouseMove(&afterReleaseMove);
|
||||||
|
REQUIRE(view.horizontalScrollBar()->value() == h1);
|
||||||
|
REQUIRE(view.verticalScrollBar()->value() == v1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoomGraphicsView drawBackground renders grid over background") {
|
||||||
|
auto tc = TestClient::Create();
|
||||||
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
ensureApp();
|
||||||
|
|
||||||
|
WarpGraphModel model(tc.client.get());
|
||||||
|
QtNodes::BasicGraphicsScene scene(model);
|
||||||
|
TestZoomGraphicsView view(&scene);
|
||||||
|
view.resize(320, 200);
|
||||||
|
view.setupScale(1.0);
|
||||||
|
|
||||||
|
auto const &style = QtNodes::StyleCollection::flowViewStyle();
|
||||||
|
QImage image(320, 200, QImage::Format_ARGB32_Premultiplied);
|
||||||
|
image.fill(style.BackgroundColor);
|
||||||
|
|
||||||
|
{
|
||||||
|
QPainter qp(&image);
|
||||||
|
view.dispatchDrawBackground(&qp, QRectF(0.0, 0.0, 320.0, 200.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRE(countPixelsDifferentFrom(image, style.BackgroundColor) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("preset saves and loads volume state") {
|
TEST_CASE("preset saves and loads volume state") {
|
||||||
auto tc = TestClient::Create();
|
auto tc = TestClient::Create();
|
||||||
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue