mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-10 03:53:26 -08:00
[Tests] Drive item views from a playback (#7819)
QtTesting's own `currentChanged` command aborts on a one-element row list, so
pqWidgetTestEventPlayer drives views itself:
- selectRow selects a row through the selection model, the whole row, without a
mouse event
- clickRow left-clicks the centre of a cell, so the view's own handlers run
- doubleClickRow double-clicks it, for views whose double click acts
A tree addresses a row by the path of rows leading to it ("2/0" is the first
child of the third top-level row), and a click opens a collapsed path first.
Rows are model rows, so a filtered or sorted model counts the rows it shows.
Asserting what a command did needs QtTesting's check players, which the XML
source never reached: it marked every event an action, so `modelRowCount` was
rejected as unknown. An event now carries `type="check"` to be played as a
check.
Testing/item_view_test.xml drives the grouped settings navigation through
selectRow and clickRow and asserts the page stack followed.
Testing/deck_editor_double_click_test.xml proves the difference between the two
clicks: a single click on a card database row only selects it, a double click
adds the card to the deck.
This commit is contained in:
parent
f089414f1c
commit
0498ea8907
7 changed files with 279 additions and 3 deletions
|
|
@ -1,12 +1,17 @@
|
|||
#include "pq_widget_test_event_player.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMouseEvent>
|
||||
#include <QPixmap>
|
||||
#include <QTextStream>
|
||||
#include <QTreeView>
|
||||
#include <QWidget>
|
||||
#include <pqObjectNaming.h>
|
||||
|
||||
|
|
@ -111,6 +116,15 @@ bool pqWidgetTestEventPlayer::playEvent(QObject *object, const QString &command,
|
|||
if (command == "resize") {
|
||||
return run(playResize(object, arguments));
|
||||
}
|
||||
if (command == "selectRow") {
|
||||
return run(playSelectRow(object, arguments));
|
||||
}
|
||||
if (command == "clickRow") {
|
||||
return run(playClickRow(object, arguments, false));
|
||||
}
|
||||
if (command == "doubleClickRow") {
|
||||
return run(playClickRow(object, arguments, true));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +213,141 @@ bool pqWidgetTestEventPlayer::playResize(QObject *object, const QString &argumen
|
|||
return true;
|
||||
}
|
||||
|
||||
bool pqWidgetTestEventPlayer::playSelectRow(QObject *object, const QString &arguments)
|
||||
{
|
||||
QAbstractItemView *view = itemViewFor(object);
|
||||
if (!view) {
|
||||
qCCritical(PQ_WIDGET_TEST_EVENT_PLAYER) << "selectRow: target is not an item view";
|
||||
return false;
|
||||
}
|
||||
|
||||
QModelIndex index;
|
||||
if (!resolveCell(view, arguments, index)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
view->setCurrentIndex(index);
|
||||
// QtTesting's `currentChanged` crashes on a one-element row list, so select
|
||||
// through the selection model.
|
||||
view->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
|
||||
view->scrollTo(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pqWidgetTestEventPlayer::playClickRow(QObject *object, const QString &arguments, bool doubleClick)
|
||||
{
|
||||
QAbstractItemView *view = itemViewFor(object);
|
||||
if (!view) {
|
||||
qCCritical(PQ_WIDGET_TEST_EVENT_PLAYER) << "clickRow: target is not an item view";
|
||||
return false;
|
||||
}
|
||||
|
||||
QModelIndex index;
|
||||
if (!resolveCell(view, arguments, index)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return clickCell(view, index, doubleClick);
|
||||
}
|
||||
|
||||
QAbstractItemView *pqWidgetTestEventPlayer::itemViewFor(QObject *object)
|
||||
{
|
||||
if (auto *view = qobject_cast<QAbstractItemView *>(object)) {
|
||||
return view;
|
||||
}
|
||||
// A recording names the viewport of an item view; the view is its parent.
|
||||
return qobject_cast<QAbstractItemView *>(object ? object->parent() : nullptr);
|
||||
}
|
||||
|
||||
bool pqWidgetTestEventPlayer::resolveCell(QAbstractItemView *view, const QString &arguments, QModelIndex &index)
|
||||
{
|
||||
QAbstractItemModel *model = view->model();
|
||||
if (!model) {
|
||||
qCCritical(PQ_WIDGET_TEST_EVENT_PLAYER) << "item view has no model:" << view;
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList parts = arguments.split(QLatin1Char(','));
|
||||
if (parts.isEmpty() || parts.size() > 2) {
|
||||
qCCritical(PQ_WIDGET_TEST_EVENT_PLAYER)
|
||||
<< "cell must be 'row[/row...]' or 'row[/row...],column', got:" << arguments;
|
||||
return false;
|
||||
}
|
||||
|
||||
int column = 0;
|
||||
if (parts.size() == 2) {
|
||||
bool okColumn = false;
|
||||
column = parts.at(1).trimmed().toInt(&okColumn);
|
||||
if (!okColumn || column < 0) {
|
||||
qCCritical(PQ_WIDGET_TEST_EVENT_PLAYER) << "invalid column in" << arguments;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// "2/0" is the first child of the third top-level row. A tree addresses a
|
||||
// row by the path of rows leading to it.
|
||||
const QStringList rows = parts.at(0).split(QLatin1Char('/'));
|
||||
index = QModelIndex();
|
||||
for (int depth = 0; depth < rows.size(); ++depth) {
|
||||
bool okRow = false;
|
||||
const int row = rows.at(depth).trimmed().toInt(&okRow);
|
||||
const int columnCount = model->columnCount(index);
|
||||
if (!okRow || row < 0 || row >= model->rowCount(index)) {
|
||||
qCCritical(PQ_WIDGET_TEST_EVENT_PLAYER) << "invalid row" << rows.at(depth) << "in" << arguments
|
||||
<< "- the parent has" << model->rowCount(index) << "rows";
|
||||
return false;
|
||||
}
|
||||
if (column >= columnCount) {
|
||||
qCCritical(PQ_WIDGET_TEST_EVENT_PLAYER)
|
||||
<< "invalid column" << column << "in" << arguments << "- the row has" << columnCount << "columns";
|
||||
return false;
|
||||
}
|
||||
|
||||
index = model->index(row, depth == rows.size() - 1 ? column : 0, index);
|
||||
if (!index.isValid()) {
|
||||
qCCritical(PQ_WIDGET_TEST_EVENT_PLAYER) << "no model index for" << arguments;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pqWidgetTestEventPlayer::clickCell(QAbstractItemView *view, const QModelIndex &index, bool doubleClick)
|
||||
{
|
||||
// A collapsed tree has no rect for a child, so open the path to it first.
|
||||
if (auto *tree = qobject_cast<QTreeView *>(view); tree && index.parent().isValid()) {
|
||||
tree->expand(index.parent());
|
||||
}
|
||||
view->scrollTo(index);
|
||||
// scrollTo only requests the scroll, so the visual rect is stale until the
|
||||
// view has laid out again.
|
||||
QApplication::processEvents();
|
||||
|
||||
const QRect rect = view->visualRect(index);
|
||||
if (!rect.isValid() || rect.isEmpty()) {
|
||||
qCCritical(PQ_WIDGET_TEST_EVENT_PLAYER) << "cell" << index << "is not visible in" << view;
|
||||
return false;
|
||||
}
|
||||
|
||||
QWidget *viewport = view->viewport();
|
||||
const QPoint pos = rect.center();
|
||||
const auto send = [viewport, &pos](QEvent::Type type) {
|
||||
QMouseEvent event(type, QPointF(pos), QPointF(viewport->mapToGlobal(pos)), Qt::LeftButton, Qt::LeftButton,
|
||||
Qt::NoModifier);
|
||||
QApplication::sendEvent(viewport, &event);
|
||||
};
|
||||
|
||||
// A double click is press/release, then the double-click event and its
|
||||
// release, as a recording produces it.
|
||||
send(QEvent::MouseButtonPress);
|
||||
send(QEvent::MouseButtonRelease);
|
||||
if (doubleClick) {
|
||||
send(QEvent::MouseButtonDblClick);
|
||||
send(QEvent::MouseButtonRelease);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pqWidgetTestEventPlayer::resolveOutputPath(const QString &command, const QString &path, QString &resolved)
|
||||
{
|
||||
if (path.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,17 @@
|
|||
|
||||
#include "pqWidgetEventPlayer.h"
|
||||
|
||||
class QAbstractItemView;
|
||||
class QModelIndex;
|
||||
|
||||
/**
|
||||
* Widget-level commands for the UI tests.
|
||||
*
|
||||
* pqCockatriceEventPlayer handles cards and zones inside the game scene. This
|
||||
* player covers the rest of the client: capturing a widget as an image,
|
||||
* dumping the paths QtTesting addresses widgets by, and sizing the window.
|
||||
* dumping the paths QtTesting addresses widgets by, sizing the window, and
|
||||
* driving item views, whose selection QtTesting's own `currentChanged` command
|
||||
* cannot handle.
|
||||
*
|
||||
* Commands:
|
||||
* screenshot "<path>"
|
||||
|
|
@ -20,6 +25,17 @@
|
|||
* addresses it by, for writing a test
|
||||
* resize "WIDTHxHEIGHT"
|
||||
* resizes the addressed widget's top-level window
|
||||
* selectRow "row[/row...][,column]"
|
||||
* selects that row of an item view, the whole row, without a mouse event
|
||||
* clickRow "row[/row...][,column]"
|
||||
* left-clicks the centre of that cell, so the view's own handlers run
|
||||
* doubleClickRow "row[/row...][,column]"
|
||||
* double-clicks it, for rows that act on double click
|
||||
*
|
||||
* A tree addresses a row by the path of rows leading to it: "2/0" is the first
|
||||
* child of the third top-level row. A click opens a collapsed path first.
|
||||
* Rows are model rows of the addressed view, so a filtered or sorted model
|
||||
* counts the rows it shows.
|
||||
*/
|
||||
class pqWidgetTestEventPlayer : public pqWidgetEventPlayer
|
||||
{
|
||||
|
|
@ -37,7 +53,15 @@ private:
|
|||
bool playScreenshot(QObject *object, const QString &arguments);
|
||||
bool playDumpTree(QObject *object, const QString &arguments);
|
||||
bool playResize(QObject *object, const QString &arguments);
|
||||
bool playSelectRow(QObject *object, const QString &arguments);
|
||||
bool playClickRow(QObject *object, const QString &arguments, bool doubleClick);
|
||||
|
||||
/** @brief The item view the event is addressed at, following a viewport to its view. */
|
||||
static QAbstractItemView *itemViewFor(QObject *object);
|
||||
/** @brief Resolves "row[,column]" to a valid model index of @p view. */
|
||||
static bool resolveCell(QAbstractItemView *view, const QString &arguments, QModelIndex &index);
|
||||
/** @brief Sends the mouse events a real click on @p index produces. */
|
||||
static bool clickCell(QAbstractItemView *view, const QModelIndex &index, bool doubleClick);
|
||||
/** @brief Expands $NAME / ${NAME} and creates the file's parent directory. */
|
||||
static bool resolveOutputPath(const QString &command, const QString &path, QString &resolved);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -85,6 +85,13 @@ int XMLEventSource::getNextEvent(QString &widget, QString &command, QString &arg
|
|||
|
||||
const QXmlStreamAttributes attributes = xmlStream->attributes();
|
||||
eventType = pqEventTypes::ACTION_EVENT;
|
||||
// QtTesting separates what a user does (an action) from what the UI must
|
||||
// show (a check), and its check players are only reached with a check
|
||||
// event. Without this attribute every event is an action, so the check
|
||||
// commands (`modelRowCount`, `modelItemData`) cannot be used from a test.
|
||||
if (attributes.value(QStringLiteral("type")) == QLatin1String("check")) {
|
||||
eventType = pqEventTypes::CHECK_EVENT;
|
||||
}
|
||||
widget = attributes.value(QStringLiteral("widget")).toString();
|
||||
command = attributes.value(QStringLiteral("command")).toString();
|
||||
if (command.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -326,8 +326,10 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
|
||||
// Create widgets for both layouts to make splitter work correctly
|
||||
auto *topWidget = new QWidget;
|
||||
topWidget->setObjectName("deckInfoPane");
|
||||
topWidget->setLayout(upperLayout);
|
||||
auto *bottomWidget = new QWidget;
|
||||
bottomWidget->setObjectName("deckListPane");
|
||||
bottomWidget->setLayout(lowerLayout);
|
||||
|
||||
auto *split = new QSplitter;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue