8#include <QtCore/qfile.h>
9#include <QtCore/qjsonarray.h>
10#include <QtCore/qjsondocument.h>
11#include <QtCore/qpoint.h>
12#include <QtCore/qstringlist.h>
13#include <QtCore/qthread.h>
21using namespace Qt::StringLiterals;
22using namespace std::chrono_literals;
26QRegularExpression caseInsensitivePattern(
const QString &pattern)
28 return QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption);
31QRect parseUiTestBounds(
const QString &bounds)
33 static const QRegularExpression pattern(u"\\[(-?\\d+),(-?\\d+)\\]\\[(-?\\d+),(-?\\d+)\\]"_s);
34 const QRegularExpressionMatch match = pattern.match(bounds);
35 if (!match.hasMatch())
38 QPoint(match.captured(1).toInt(), match.captured(2).toInt()),
39 QPoint(match.captured(3).toInt(), match.captured(4).toInt()));
42QJsonObject dumpScreenLayout(
const Hdc &hdc)
44 constexpr auto layoutPathOnDevice =
"/data/local/tmp/harmonyostestrunner-layout.json"_L1;
46 hdc.shell({ u"uitest"_s, u"dumpLayout"_s, u"-p"_s, layoutPathOnDevice });
47 return QJsonDocument::fromJson(readDeviceFile(hdc, layoutPathOnDevice).toUtf8()).object();
50void clickAt(
const Hdc &hdc, QPoint point)
52 hdc.shell({ u"uitest"_s, u"uiInput"_s, u"click"_s,
53 QString::number(point.x()), QString::number(point.y()) });
58BlockingTestDialogs::BlockingTestDialogs(
const QString &testConfigPath, Hdc hdc)
59 : m_hdc(std::move(hdc))
61 loadDeclarations(testConfigPath);
64bool BlockingTestDialogs::isEmpty()
const
66 return m_dialogs.isEmpty();
69void BlockingTestDialogs::loadDeclarations(
const QString &testConfigPath)
71 if (testConfigPath.isEmpty())
74 QFile configFile(testConfigPath);
75 if (!configFile.open(QIODevice::ReadOnly)) {
76 fprintf(stderr,
"harmonyostestrunner: cannot read the test config %s\n",
77 qPrintable(testConfigPath));
81 QJsonParseError parseError;
82 const QJsonDocument document = QJsonDocument::fromJson(configFile.readAll(), &parseError);
83 if (parseError.error != QJsonParseError::NoError) {
84 fprintf(stderr,
"harmonyostestrunner: test config %s is not valid JSON: %s\n",
85 qPrintable(testConfigPath), qPrintable(parseError.errorString()));
89 const QJsonArray declaredDialogs =
90 document.object().value(u"blockingTestDialogs"_s).toArray();
91 for (
const QJsonValue &value : declaredDialogs) {
92 const std::optional<Dialog> dialog = parseDialog(value.toObject());
93 if (dialog.has_value())
94 m_dialogs.append(*dialog);
98std::optional<BlockingTestDialogs::Dialog>
99BlockingTestDialogs::parseDialog(
const QJsonObject &declaration)
102 dialog.name = declaration.value(u"name"_s).toString();
103 dialog.prompt = caseInsensitivePattern(declaration.value(u"prompt"_s).toString());
104 const QJsonArray buttons = declaration.value(u"buttons"_s).toArray();
105 for (
const QJsonValue &button : buttons)
106 dialog.buttons.append(caseInsensitivePattern(button.toString()));
107 dialog.position = declaration.value(u"position"_s).toString();
109 if (dialog.prompt.pattern().isEmpty() || dialog.buttons.isEmpty()) {
110 fprintf(stderr,
"harmonyostestrunner: ignoring blocking test dialog '%s': "
111 "prompt and button patterns are required\n", qPrintable(dialog.name));
117void BlockingTestDialogs::collectTexts(
const QJsonObject &node, QList<ScreenText> &texts)
119 const QJsonObject attributes = node.value(u"attributes"_s).toObject();
120 const QString text = attributes.value(u"text"_s).toString().trimmed();
122 texts.append({ text, parseUiTestBounds(attributes.value(u"bounds"_s).toString()) });
124 const QJsonArray children = node.value(u"children"_s).toArray();
125 for (
const QJsonValue &child : children)
126 collectTexts(child.toObject(), texts);
129QList<BlockingTestDialogs::ScreenText> BlockingTestDialogs::dumpScreenTexts()
const
131 QList<ScreenText> texts;
132 collectTexts(dumpScreenLayout(m_hdc), texts);
136bool BlockingTestDialogs::anyTextMatches(
137 const QList<ScreenText> &texts,
const QRegularExpression &pattern)
139 return std::any_of(texts.cbegin(), texts.cend(), [&pattern](
const ScreenText &screenText) {
140 return pattern.match(screenText.text).hasMatch();
144std::optional<BlockingTestDialogs::ScreenText>
145BlockingTestDialogs::findButton(
146 const QList<ScreenText> &texts,
const QRegularExpression &pattern,
147 const QString &position)
149 QList<ScreenText> candidates;
150 for (
const ScreenText &screenText : texts) {
151 const QRegularExpressionMatch match = pattern.match(screenText.text);
152 if (match.hasMatch() && match.capturedLength() == screenText.text.size()
153 && !screenText.bounds.isNull())
154 candidates.append(screenText);
156 if (candidates.isEmpty())
159 auto byHorizontalCentre = [](
const ScreenText &left,
const ScreenText &right) {
160 return left.bounds.center().x() < right.bounds.center().x();
162 if (position == u"leftmost"_s)
163 return *
std::min_element(candidates.cbegin(), candidates.cend(), byHorizontalCentre);
164 return *
std::max_element(candidates.cbegin(), candidates.cend(), byHorizontalCentre);
167bool BlockingTestDialogs::pressButtons(
const Dialog &dialog,
const QList<ScreenText> &texts)
const
169 constexpr auto delayBetweenButtons = 700ms;
171 bool pressedAnything =
false;
172 for (
const QRegularExpression &buttonPattern : dialog.buttons) {
173 const std::optional<ScreenText> button = findButton(texts, buttonPattern, dialog.position);
174 if (!button.has_value())
177 const QPoint centre = button->bounds.center();
178 fprintf(stderr,
"harmonyostestrunner: answering blocking test dialog '%s': pressing "
179 "\"%s\" at %d,%d\n", qPrintable(dialog.name), qPrintable(button->text),
180 centre.x(), centre.y());
181 clickAt(m_hdc, centre);
182 pressedAnything =
true;
183 QThread::sleep(delayBetweenButtons);
185 return pressedAnything;
188void BlockingTestDialogs::reportUnmatchedDialog(
const QList<ScreenText> &texts)
190 constexpr int maximumTextsInReportedDialog = 8;
192 if (texts.size() > maximumTextsInReportedDialog)
195 static const QRegularExpression declineButton =
196 caseInsensitivePattern(u"^(cancel|deny|not now|later)$"_s);
197 if (!anyTextMatches(texts, declineButton))
200 QStringList onScreen;
201 for (
const ScreenText &screenText : texts)
202 onScreen << screenText.text;
204 const QString joined = onScreen.join(u" | "_s);
205 if (joined == m_lastReportedUnknownDialog)
208 m_lastReportedUnknownDialog = joined;
209 fprintf(stderr,
"harmonyostestrunner: dialog on screen matches no declaration, leaving it "
210 "alone: %s\n", qPrintable(joined));
213bool BlockingTestDialogs::answerVisibleDialog()
215 if (m_dialogs.isEmpty())
218 const QList<ScreenText> texts = dumpScreenTexts();
222 for (
const Dialog &dialog : m_dialogs) {
223 if (!anyTextMatches(texts, dialog.prompt))
226 const bool pressedAnything = pressButtons(dialog, texts);
227 if (!pressedAnything) {
228 fprintf(stderr,
"harmonyostestrunner: blocking test dialog '%s' is on screen but none "
229 "of its buttons matched the declaration\n", qPrintable(dialog.name));
231 return pressedAnything;
234 reportUnmatchedDialog(texts);