[Tests] Play card interactions through a Cockatrice event player (#7697)

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-11-04 06:23:45 +01:00 • committed by GitHub
parent 34201122ec
commit 3fa8d78786
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1119 additions and 3 deletions

View file

@ -71,6 +71,8 @@ void ToggleButton::setState(bool _state)
DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
: QWidget(nullptr), visualDeckStorageWidget(nullptr), parentGame(parent), playerId(_playerId)
{
setObjectName("deckViewContainer");
loadLocalButton = new QPushButton;
loadRemoteButton = new QPushButton;
loadFromClipboardButton = new QPushButton;
@ -80,6 +82,14 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
forceStartGameButton = new QPushButton;
sideboardLockButton = new ToggleButton;
loadLocalButton->setObjectName("loadLocalButton");
loadRemoteButton->setObjectName("loadRemoteButton");
loadFromClipboardButton->setObjectName("loadFromClipboardButton");
readyStartButton->setObjectName("readyStartButton");
forceStartGameButton->setObjectName("forceStartGameButton");
unloadDeckButton->setObjectName("unloadDeckButton");
sideboardLockButton->setObjectName("sideboardLockButton");
connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck);
connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck);
connect(loadFromClipboardButton, &QPushButton::clicked, this, &DeckViewContainer::loadFromClipboard);

View file

@ -14,6 +14,7 @@ class TabGame;
TabbedDeckViewContainer::TabbedDeckViewContainer(int _playerId, TabGame *parent)
: QTabWidget(nullptr), playerId(_playerId), parentGame(parent)
{
setObjectName(QString("tabbedDeckView_%1").arg(_playerId));
setTabsClosable(true);
connect(this, &QTabWidget::tabCloseRequested, this, &TabbedDeckViewContainer::closeTab);

View file

@ -48,6 +48,7 @@ protected:
GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, parent), rubberBand(0)
{
setObjectName("gameView");
setBackgroundBrush(QBrush(QColor(0, 0, 0)));
setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing);
setViewportUpdateMode(BoundingRectViewportUpdate);

View file

@ -0,0 +1,628 @@
#include "pq_cockatrice_event_player.h"
#include "../../interface/widgets/tabs/tab_game.h"
#include "board/card_item.h"
#include "game_scene.h"
#include "game_view.h"
#include "zones/card_zone.h"
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QAction>
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QLoggingCategory>
#include <QMenu>
#include <QMouseEvent>
#include <QTreeWidget>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/game_state.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/player/player_manager.h>
Q_LOGGING_CATEGORY(PQ_COCKATRICE_EVENT_PLAYER, "cockatrice.guiTest.eventPlayer")
namespace
{
/**
* Strips Qt's mnemonic markers so a test can name a menu entry by the label the
* user sees ("Turn Over" rather than "T&urn Over").
*/
QString normalizeActionText(const QString &text)
{
QString normalized = text;
normalized.remove('&');
return normalized.simplified();
}
/**
* Maps a printable character onto the key a US keyboard would report for it, so
* typing produces the same QKeyEvents a recording would.
*/
Qt::Key keyForCharacter(QChar character)
{
if (character.isLetter()) {
return static_cast<Qt::Key>(Qt::Key_A + (character.toUpper().unicode() - 'A'));
}
if (character.isDigit()) {
return static_cast<Qt::Key>(Qt::Key_0 + (character.unicode() - '0'));
}
switch (character.unicode()) {
case ' ':
return Qt::Key_Space;
case '\n':
return Qt::Key_Return;
case '\t':
return Qt::Key_Tab;
case '-':
return Qt::Key_Minus;
case '_':
return Qt::Key_Underscore;
case '/':
return Qt::Key_Slash;
case ',':
return Qt::Key_Comma;
case '.':
return Qt::Key_Period;
case '\'':
return Qt::Key_Apostrophe;
case ';':
return Qt::Key_Semicolon;
case ':':
return Qt::Key_Colon;
case '"':
return Qt::Key_QuoteDbl;
case '&':
return Qt::Key_Ampersand;
case '(':
return Qt::Key_ParenLeft;
case ')':
return Qt::Key_ParenRight;
case '[':
return Qt::Key_BracketLeft;
case ']':
return Qt::Key_BracketRight;
default:
return Qt::Key_unknown;
}
}
/** Depth-first search for the first model index whose label matches @p text. */
QModelIndex findModelIndexByText(const QAbstractItemModel *model, const QString &text, const QModelIndex &parent = {})
{
for (int row = 0; row < model->rowCount(parent); ++row) {
const QModelIndex index = model->index(row, 0, parent);
if (index.data(Qt::DisplayRole).toString() == text) {
return index;
}
const QModelIndex child = findModelIndexByText(model, text, index);
if (child.isValid()) {
return child;
}
}
return {};
}
bool needsShift(QChar character)
{
if (character.isUpper()) {
return true;
}
return QStringLiteral("_:\"&()").contains(character);
}
} // namespace
pqCockatriceEventPlayer::pqCockatriceEventPlayer(QObject *parent) : pqWidgetEventPlayer(parent)
{
}
bool pqCockatriceEventPlayer::playEvent(QObject *object, const QString &command, const QString &arguments, bool &error)
{
// A known command that fails is a handled-but-failed event: QtTesting logs
// "Unhandled event" when playEvent returns false, so report through `error`
// (the contract playExpectProperty already follows) and still return true.
const auto run = [&error](bool ok) {
if (!ok) {
error = true;
}
return true;
};
if (command == "clickCard") {
return run(playClickCard(object, arguments));
} else if (command == "flipCard") {
return run(playFlipCard(object, arguments));
} else if (command == "cardAction") {
return run(playCardAction(object, arguments));
} else if (command == "moveCard") {
return run(playMoveCard(object, arguments));
} else if (command == "drawArrow") {
return run(playDrawArrow(object, arguments));
} else if (command == "createToken") {
return run(playCreateToken(object, arguments));
} else if (command == "selectItem") {
return run(playSelectItem(object, arguments));
} else if (command == "typeText") {
return run(playTypeText(object, arguments));
} else if (command == "expectProperty") {
return playExpectProperty(object, arguments, error);
}
return false;
}
QGraphicsItem *
pqCockatriceEventPlayer::findCardInScene(QGraphicsScene *scene, const QString &cardName, const QString &zoneName)
{
for (QGraphicsItem *gi : scene->items()) {
if (gi->type() != typeCard) {
continue;
}
auto *card = static_cast<CardItem *>(gi);
if (!card->getZone() || card->getZone()->getName() != zoneName) {
continue;
}
QString actualName = card->getName();
if (actualName == cardName || actualName.endsWith(" " + cardName)) {
return card;
}
}
return nullptr;
}
QGraphicsItem *pqCockatriceEventPlayer::findZoneInScene(QGraphicsScene *scene, const QString &zoneName)
{
for (QGraphicsItem *gi : scene->items()) {
if (gi->type() != typeZone) {
continue;
}
auto *zone = static_cast<CardZone *>(gi);
if (zone->getLogic() && zone->getLogic()->getName() == zoneName) {
return zone;
}
}
return nullptr;
}
QAction *pqCockatriceEventPlayer::findActionInMenu(QMenu *menu, const QString &actionText)
{
if (!menu || actionText.isEmpty()) {
return nullptr;
}
for (QAction *action : menu->actions()) {
if (action->isSeparator()) {
continue;
}
if (normalizeActionText(action->text()).contains(actionText, Qt::CaseInsensitive)) {
return action;
}
if (QMenu *subMenu = action->menu()) {
if (QAction *found = findActionInMenu(subMenu, actionText)) {
return found;
}
}
}
return nullptr;
}
QMenu *pqCockatriceEventPlayer::visiblePopupMenu()
{
for (QWidget *w : QApplication::topLevelWidgets()) {
auto *menu = qobject_cast<QMenu *>(w);
if (menu && menu->isVisible()) {
return menu;
}
}
return nullptr;
}
QPoint pqCockatriceEventPlayer::viewportPoint(QGraphicsView *view, const QGraphicsItem *item)
{
return view->mapFromScene(item->sceneBoundingRect().center());
}
void pqCockatriceEventPlayer::sendMouseEvent(QGraphicsView *view,
QEvent::Type type,
const QPoint &pos,
Qt::MouseButton button,
Qt::MouseButtons buttons)
{
QMouseEvent event(type, pos, view->viewport()->mapToGlobal(pos), button, buttons, Qt::NoModifier);
qApp->notify(view->viewport(), &event);
}
bool pqCockatriceEventPlayer::playClickCard(QObject *object, const QString &arguments)
{
auto *gv = qobject_cast<QGraphicsView *>(object);
if (!gv) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: target widget is not a QGraphicsView";
return false;
}
QStringList parts = arguments.split(',');
if (parts.size() < 2) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: arguments must be 'cardName,zoneName', got:" << arguments;
return false;
}
QGraphicsScene *scene = gv->scene();
if (!scene) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: no scene on the QGraphicsView";
return false;
}
auto *target = findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed());
if (!target) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
<< "clickCard: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed();
return false;
}
const QPoint pos = viewportPoint(gv, target);
// A double click is the play gesture from hand: press/release, then the
// double-click event and its release, exactly as a recording produces it.
sendMouseEvent(gv, QEvent::MouseButtonPress, pos, Qt::LeftButton, Qt::LeftButton);
sendMouseEvent(gv, QEvent::MouseButtonRelease, pos, Qt::LeftButton, Qt::LeftButton);
sendMouseEvent(gv, QEvent::MouseButtonDblClick, pos, Qt::LeftButton, Qt::LeftButton);
sendMouseEvent(gv, QEvent::MouseButtonRelease, pos, Qt::LeftButton, Qt::LeftButton);
return true;
}
bool pqCockatriceEventPlayer::playFlipCard(QObject *object, const QString &arguments)
{
auto *gv = qobject_cast<QGraphicsView *>(object);
if (!gv) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: target widget is not a QGraphicsView";
return false;
}
QStringList parts = arguments.split(',');
if (parts.size() < 2) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: arguments must be 'cardName,zoneName', got:" << arguments;
return false;
}
QGraphicsScene *scene = gv->scene();
if (!scene) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: no scene";
return false;
}
auto *card = qgraphicsitem_cast<CardItem *>(findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed()));
if (!card) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
<< "flipCard: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed();
return false;
}
return invokeCardMenuAction("flipCard", gv, card, QStringLiteral("Turn Over"));
}
bool pqCockatriceEventPlayer::playCardAction(QObject *object, const QString &arguments)
{
auto *gv = qobject_cast<QGraphicsView *>(object);
if (!gv) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "cardAction: target widget is not a QGraphicsView";
return false;
}
QStringList parts = arguments.split(',');
if (parts.size() < 3) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
<< "cardAction: arguments must be 'cardName,zoneName,actionText', got:" << arguments;
return false;
}
QGraphicsScene *scene = gv->scene();
if (!scene) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "cardAction: no scene";
return false;
}
auto *card = qgraphicsitem_cast<CardItem *>(findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed()));
if (!card) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
<< "cardAction: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed();
return false;
}
return invokeCardMenuAction("cardAction", gv, card, parts.mid(2).join(',').trimmed());
}
bool pqCockatriceEventPlayer::invokeCardMenuAction(const char *command,
QGraphicsView *view,
CardItem *card,
const QString &actionText)
{
const QPoint pos = viewportPoint(view, card);
sendMouseEvent(view, QEvent::MouseButtonPress, pos, Qt::RightButton, Qt::RightButton);
sendMouseEvent(view, QEvent::MouseButtonRelease, pos, Qt::RightButton, Qt::NoButton);
qApp->processEvents();
QMenu *menu = visiblePopupMenu();
if (!menu) {
qCWarning(PQ_COCKATRICE_EVENT_PLAYER) << command << ": no popup menu appeared after right-click";
return false;
}
QAction *action = findActionInMenu(menu, actionText);
if (!action) {
qCWarning(PQ_COCKATRICE_EVENT_PLAYER) << command << ": no context menu action matching" << actionText;
menu->close();
return false;
}
action->trigger();
menu->close();
qApp->processEvents();
return true;
}
bool pqCockatriceEventPlayer::playMoveCard(QObject *object, const QString &arguments)
{
auto *gv = qobject_cast<QGraphicsView *>(object);
if (!gv) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: target widget is not a QGraphicsView";
return false;
}
QStringList parts = arguments.split(',');
if (parts.size() < 3) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
<< "moveCard: arguments must be 'cardName,srcZoneName,dstZoneName', got:" << arguments;
return false;
}
const QString cardName = parts[0].trimmed();
const QString srcZoneName = parts[1].trimmed();
const QString dstZoneName = parts[2].trimmed();
QGraphicsScene *scene = gv->scene();
if (!scene) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: no scene";
return false;
}
auto *card = findCardInScene(scene, cardName, srcZoneName);
if (!card) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: card" << cardName << "not found in zone" << srcZoneName;
return false;
}
auto *dstZone = findZoneInScene(scene, dstZoneName);
if (!dstZone) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: destination zone" << dstZoneName << "not found";
return false;
}
const QPoint srcPos = viewportPoint(gv, card);
const QPoint dstPos = viewportPoint(gv, dstZone);
// A drag has to travel past Qt's start distance before the scene turns it
// into a card drag item, otherwise the move is interpreted as a click.
const int dragDistance = 2 * QApplication::startDragDistance() + 10;
const QPoint pastDragStart = srcPos + QPoint(dragDistance, 0);
sendMouseEvent(gv, QEvent::MouseButtonPress, srcPos, Qt::LeftButton, Qt::LeftButton);
sendMouseEvent(gv, QEvent::MouseMove, pastDragStart, Qt::LeftButton, Qt::LeftButton);
qApp->processEvents();
sendMouseEvent(gv, QEvent::MouseMove, dstPos, Qt::LeftButton, Qt::LeftButton);
qApp->processEvents();
sendMouseEvent(gv, QEvent::MouseButtonRelease, dstPos, Qt::LeftButton, Qt::NoButton);
qApp->processEvents();
return true;
}
bool pqCockatriceEventPlayer::playDrawArrow(QObject *object, const QString &arguments)
{
auto *gv = qobject_cast<QGraphicsView *>(object);
if (!gv) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "drawArrow: target widget is not a QGraphicsView";
return false;
}
QStringList parts = arguments.split(',');
if (parts.size() < 4) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
<< "drawArrow: arguments must be 'srcCard,srcZone,dstCard,dstZone', got:" << arguments;
return false;
}
const QString srcCardName = parts[0].trimmed();
const QString srcZoneName = parts[1].trimmed();
const QString dstCardName = parts[2].trimmed();
const QString dstZoneName = parts[3].trimmed();
QGraphicsScene *scene = gv->scene();
if (!scene) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "drawArrow: no scene";
return false;
}
auto *srcCard = findCardInScene(scene, srcCardName, srcZoneName);
if (!srcCard) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
<< "drawArrow: source card" << srcCardName << "not found in zone" << srcZoneName;
return false;
}
auto *dstCard = findCardInScene(scene, dstCardName, dstZoneName);
if (!dstCard) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
<< "drawArrow: dest card" << dstCardName << "not found in zone" << dstZoneName;
return false;
}
const QPoint srcPos = viewportPoint(gv, srcCard);
const QPoint dstPos = viewportPoint(gv, dstCard);
const int dragDistance = 2 * QApplication::startDragDistance() + 10;
const QPoint pastDragStart = srcPos + QPoint(dragDistance, 0);
sendMouseEvent(gv, QEvent::MouseButtonPress, srcPos, Qt::RightButton, Qt::RightButton);
sendMouseEvent(gv, QEvent::MouseMove, pastDragStart, Qt::RightButton, Qt::RightButton);
qApp->processEvents();
sendMouseEvent(gv, QEvent::MouseMove, dstPos, Qt::RightButton, Qt::RightButton);
sendMouseEvent(gv, QEvent::MouseButtonRelease, dstPos, Qt::RightButton, Qt::NoButton);
qApp->processEvents();
return true;
}
bool pqCockatriceEventPlayer::playCreateToken(QObject *object, const QString &arguments)
{
auto *gv = qobject_cast<QGraphicsView *>(object);
if (!gv) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: target widget is not a QGraphicsView";
return false;
}
QStringList parts = arguments.split(',');
if (parts.size() < 3) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: arguments must be 'name,color,pt', got:" << arguments;
return false;
}
QString tokenName = parts[0].trimmed();
QString tokenColor = parts[1].trimmed();
QString tokenPt = parts[2].trimmed();
TabGame *tabGame = nullptr;
QWidget *w = gv;
while (w) {
tabGame = qobject_cast<TabGame *>(w);
if (tabGame) {
break;
}
w = w->parentWidget();
}
if (!tabGame) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: TabGame not found";
return false;
}
AbstractGame *game = tabGame->getGame();
if (!game) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: no game";
return false;
}
int activePlayerId = game->getGameState()->getActivePlayer();
PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(activePlayerId);
if (!player) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: no active local player";
return false;
}
TokenInfo info;
info.name = tokenName;
info.color = tokenColor;
info.pt = tokenPt;
info.destroy = true;
info.faceDown = false;
player->getPlayerActions()->actCreateToken(info);
for (int i = 0; i < 10; ++i) {
qApp->processEvents();
}
return true;
}
bool pqCockatriceEventPlayer::playSelectItem(QObject *object, const QString &arguments)
{
const QString text = arguments.trimmed();
if (text.isEmpty()) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: needs the label of the item to select";
return false;
}
if (auto *tree = qobject_cast<QTreeWidget *>(object)) {
QTreeWidgetItemIterator item(tree);
while (*item) {
if ((*item)->text(0) == text) {
tree->setCurrentItem(*item);
qApp->processEvents();
return true;
}
++item;
}
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: no tree item labelled" << text;
return false;
}
if (auto *view = qobject_cast<QAbstractItemView *>(object)) {
const QModelIndex index = findModelIndexByText(view->model(), text);
if (!index.isValid()) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: no item labelled" << text;
return false;
}
view->setCurrentIndex(index);
qApp->processEvents();
return true;
}
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: target is neither a tree widget nor an item view";
return false;
}
bool pqCockatriceEventPlayer::playTypeText(QObject *object, const QString &arguments)
{
auto *widget = qobject_cast<QWidget *>(object);
if (!widget) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "typeText: target is not a QWidget";
return false;
}
for (const QChar character : arguments) {
const Qt::Key key = keyForCharacter(character);
const Qt::KeyboardModifiers modifiers = needsShift(character) ? Qt::ShiftModifier : Qt::NoModifier;
QKeyEvent press(QEvent::KeyPress, key, modifiers, QString(character));
QKeyEvent release(QEvent::KeyRelease, key, modifiers, QString(character));
qApp->notify(widget, &press);
qApp->notify(widget, &release);
}
qApp->processEvents();
return true;
}
bool pqCockatriceEventPlayer::playExpectProperty(QObject *object, const QString &arguments, bool &error)
{
if (!object) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "expectProperty: no target object";
error = true;
return true;
}
QStringList parts = arguments.split(',');
if (parts.size() < 2) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
<< "expectProperty: arguments must be 'propertyName,expectedValue', got:" << arguments;
error = true;
return true;
}
const QString propertyName = parts[0].trimmed();
const QString expected = parts.mid(1).join(',');
const QVariant actual = object->property(propertyName.toUtf8().constData());
if (!actual.isValid()) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
<< "expectProperty:" << object->objectName() << "has no property" << propertyName;
error = true;
return true;
}
if (actual.toString() != expected) {
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "expectProperty:" << object->objectName() << propertyName << "is"
<< actual.toString() << "but the test expected" << expected;
error = true;
return true;
}
return true;
}

View file

@ -0,0 +1,91 @@
#ifndef PQ_COCKATRICE_EVENT_PLAYER_H
#define PQ_COCKATRICE_EVENT_PLAYER_H
#include "pqWidgetEventPlayer.h"
#include <QModelIndex>
class CardItem;
class QAction;
class QGraphicsItem;
class QGraphicsScene;
class QGraphicsView;
class QMenu;
/**
* Custom event player for Cockatrice game interactions.
*
* Card commands are resolution independent: the card is looked up in the
* QGraphicsScene by name and zone, then mouse events are sent at the card's
* current viewport position. That keeps recordings usable across screen sizes
* and window geometries.
*
* Commands:
* clickCard "cardName,zoneName"
* left press/release + double click — plays a card from hand, taps a
* permanent, opens the deck view entry, depending on the zone
* flipCard "cardName,zoneName"
* right-click → "Turn Over" (shorthand for cardAction)
* cardAction "cardName,zoneName,actionText"
* right-click the card and trigger the first context menu action whose
* label contains actionText; submenus are searched as well, so
* "Add counter (A)" works
* moveCard "cardName,srcZoneName,dstZoneName"
* left-drag the card onto the destination zone — plays a card, moves it
* to the graveyard/exile, puts it back into hand, ...
* drawArrow "srcCard,srcZone,dstCard,dstZone"
* right-drag an arrow from one card to another
* createToken "name,color,pt"
* create a token through the active local player, as the table context
* menu's "Create token" dialog would
* selectItem "item text"
* selects the first item of a tree/list widget whose label matches, for
* navigating the settings navigation tree or similar label-driven views
* typeText "some text"
* types the text into the widget one key event at a time — use it for
* deck lists and search boxes instead of one event per character
* expectProperty "propertyName,expectedValue"
* assertion: playback fails unless object->property(propertyName)
* equals expectedValue — for example "count,2" on the tab widget
*/
class pqCockatriceEventPlayer : public pqWidgetEventPlayer
{
Q_OBJECT
typedef pqWidgetEventPlayer Superclass;
public:
explicit pqCockatriceEventPlayer(QObject *parent = nullptr);
using Superclass::playEvent;
bool playEvent(QObject *object, const QString &command, const QString &arguments, bool &error) override;
private:
bool playClickCard(QObject *object, const QString &arguments);
bool playFlipCard(QObject *object, const QString &arguments);
bool playCardAction(QObject *object, const QString &arguments);
bool playMoveCard(QObject *object, const QString &arguments);
bool playDrawArrow(QObject *object, const QString &arguments);
bool playCreateToken(QObject *object, const QString &arguments);
bool playSelectItem(QObject *object, const QString &arguments);
bool playTypeText(QObject *object, const QString &arguments);
bool playExpectProperty(QObject *object, const QString &arguments, bool &error);
/** @brief Triggers the context menu entry @p actionText after right-clicking @p card. */
bool invokeCardMenuAction(const char *command, QGraphicsView *view, CardItem *card, const QString &actionText);
static QGraphicsItem *findCardInScene(QGraphicsScene *scene, const QString &cardName, const QString &zoneName);
static QGraphicsItem *findZoneInScene(QGraphicsScene *scene, const QString &zoneName);
static QAction *findActionInMenu(QMenu *menu, const QString &actionText);
static QMenu *visiblePopupMenu();
/** @brief Viewport position of the item's centre, as the mouse events have to be sent. */
static QPoint viewportPoint(QGraphicsView *view, const QGraphicsItem *item);
static void sendMouseEvent(QGraphicsView *view,
QEvent::Type type,
const QPoint &pos,
Qt::MouseButton button,
Qt::MouseButtons buttons);
};
#endif // PQ_COCKATRICE_EVENT_PLAYER_H

View file

@ -28,12 +28,16 @@ class QWidget;
*/
AbstractDlgDeckTextEdit::AbstractDlgDeckTextEdit(QWidget *parent) : QDialog(parent)
{
setObjectName("dlgDeckTextEdit");
contentsEdit = new QPlainTextEdit;
contentsEdit->setObjectName("contentsEdit");
refreshButton = new QPushButton(tr("&Refresh"));
connect(refreshButton, &QPushButton::clicked, this, &AbstractDlgDeckTextEdit::actRefresh);
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttonBox->setObjectName("buttonBox");
buttonBox->addButton(refreshButton, QDialogButtonBox::ActionRole);
connect(buttonBox, &QDialogButtonBox::accepted, this, &AbstractDlgDeckTextEdit::actOK);
connect(buttonBox, &QDialogButtonBox::rejected, this, &AbstractDlgDeckTextEdit::reject);

View file

@ -16,8 +16,11 @@ class QWidget;
DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent)
{
setObjectName("localGameDialog");
numberPlayersLabel = new QLabel(tr("Players:"), this);
numberPlayersEdit = new QSpinBox(this);
numberPlayersEdit->setObjectName("numberPlayersEdit");
numberPlayersEdit->setMinimum(1);
numberPlayersEdit->setMaximum(8);
numberPlayersEdit->setValue(1);
@ -28,10 +31,12 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent)
generalGrid->addWidget(numberPlayersLabel, 0, 0);
generalGrid->addWidget(numberPlayersEdit, 0, 1);
generalGroupBox = new QGroupBox(tr("General"), this);
generalGroupBox->setObjectName("generalGroupBox");
generalGroupBox->setLayout(generalGrid);
startingLifeTotalLabel = new QLabel(tr("Starting life total:"), this);
startingLifeTotalEdit = new QSpinBox(this);
startingLifeTotalEdit->setObjectName("startingLifeTotalEdit");
startingLifeTotalEdit->setMinimum(-999999999);
startingLifeTotalEdit->setMaximum(999999999);
startingLifeTotalEdit->setValue(20);
@ -42,11 +47,14 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent)
gameSetupGrid->addWidget(startingLifeTotalLabel, 0, 0);
gameSetupGrid->addWidget(startingLifeTotalEdit, 0, 1);
gameSetupOptionsGroupBox = new QGroupBox(tr("Game setup options"), this);
gameSetupOptionsGroupBox->setObjectName("gameSetupOptionsGroupBox");
gameSetupOptionsGroupBox->setLayout(gameSetupGrid);
rememberSettingsCheckBox = new QCheckBox(tr("Remember settings"), this);
rememberSettingsCheckBox->setObjectName("rememberSettingsCheckBox");
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
buttonBox->setObjectName("buttonBox");
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgLocalGameOptions::actOK);
connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgLocalGameOptions::reject);

View file

@ -99,6 +99,7 @@ class Message;
TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
: Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr)
{
setObjectName("TabGame");
// THIS CTOR IS USED ON REPLAY
game = new Replay(this, _replay, tabSupervisor->getIsLocalGame(), &SettingsCache::instance().userInterface(),
new SettingsCardPreferenceProvider(this));
@ -116,6 +117,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
addDockWidget(Qt::BottomDockWidgetArea, replayDock);
mainWidget = new QStackedWidget(this);
mainWidget->setObjectName("mainWidget");
mainWidget->addWidget(deckViewContainerWidget);
mainWidget->addWidget(gamePlayAreaWidget);
setCentralWidget(mainWidget);
@ -144,6 +146,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
const QMap<int, QString> &_roomGameTypes)
: Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager())
{
setObjectName("TabGame");
// THIS CTOR IS USED ON GAMES
game = new Game(this, tabSupervisor->getIsLocalGame(), _clients, event, _roomGameTypes,
&SettingsCache::instance().userInterface(), new SettingsCardPreferenceProvider(this));
@ -160,6 +163,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
mainWidget = new QStackedWidget(this);
mainWidget->setObjectName("mainWidget");
mainWidget->addWidget(deckViewContainerWidget);
mainWidget->addWidget(gamePlayAreaWidget);
mainWidget->setContentsMargins(0, 0, 0, 0);
@ -1118,6 +1122,7 @@ void TabGame::createMenuItems()
connect(aFocusChat, &QAction::triggered, sayEdit, qOverload<>(&LineEditCompleter::setFocus));
phasesMenu = new TearOffMenu(this);
phasesMenu->setObjectName("phasesMenu");
for (int i = 0; i < phasesToolbar->phaseCount(); ++i) {
auto *temp = new QAction(QString(), this);
@ -1128,12 +1133,16 @@ void TabGame::createMenuItems()
phasesMenu->addSeparator();
phasesMenu->addAction(aNextPhase);
aNextPhase->setObjectName("aNextPhase");
phasesMenu->addAction(aNextPhaseAction);
aNextPhaseAction->setObjectName("aNextPhaseAction");
gameMenu = new QMenu(this);
gameMenu->setObjectName("gameMenu");
playersSeparator = gameMenu->addSeparator();
gameMenu->addMenu(phasesMenu);
gameMenu->addAction(aNextTurn);
aNextTurn->setObjectName("aNextTurn");
gameMenu->addAction(aReverseTurn);
gameMenu->addSeparator();
gameMenu->addAction(aRemoveLocalArrows);

View file

@ -56,6 +56,7 @@
#include "widgets/settings_page/abstract_settings_page.h"
#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h"
#ifdef WITH_GUI_TEST
#include "../game_graphics/pq_cockatrice_event_player.h"
#include "pqEventObserver.h"
#include "pqEventSource.h"
#include "pqEventTypes.h"
@ -689,6 +690,7 @@ MainWindow::MainWindow(QWidget *parent)
testUtility->setRecordWithDialog(true);
testUtility->addEventObserver("xml", new XMLEventObserver(this));
testUtility->addEventSource("xml", new XMLEventSource(this));
testUtility->eventPlayer()->addWidgetEventPlayer(new pqCockatriceEventPlayer(this));
#endif
}