diff --git a/app/src/main.cpp b/app/src/main.cpp index 541eb72..4d1176f 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -193,12 +193,20 @@ int main(int argc, char **argv) { QCommandLineOption sizeOpt(QStringLiteral("size"), QStringLiteral("initial window size, e.g. 480x900"), QStringLiteral("WxH")); + /* A device run cannot be handed an environment (see activityArguments()), so + * what a desktop run asks for with CST_APP_SHOT=shot.png has to be an + * argument here. */ + QCommandLineOption shotsOpt( + QStringLiteral("shots"), + QStringLiteral("--uitest: save a picture of the window at the interesting " + "points, in the workspace")); parser.addOption(wsOpt); parser.addOption(providerOpt); parser.addOption(modelOpt); parser.addOption(planOpt); parser.addOption(uitestOpt); parser.addOption(sizeOpt); + parser.addOption(shotsOpt); /* What the process was started with, plus -- on a phone -- what the * activity was started with: Qt's own applicationArguments plumbing does * not deliver the extra on the NX789J (see activityArguments()), and @@ -235,6 +243,12 @@ int main(int argc, char **argv) { } } + /* Before the window exists: maybeShot() reads this at the point a picture is + * wanted (the settings dialog, the end of the run). */ + if (parser.isSet(shotsOpt)) + qputenv("CST_APP_SHOT", + (ws + QStringLiteral("/uitest-shot.png")).toUtf8()); + #if defined(Q_OS_ANDROID) /* Before the engine is created: its settings, credentials and session store * all resolve their paths from the environment. */ diff --git a/app/src/ui/UiTest.cpp b/app/src/ui/UiTest.cpp index fbee357..cd78d64 100644 --- a/app/src/ui/UiTest.cpp +++ b/app/src/ui/UiTest.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include @@ -51,6 +52,27 @@ int g_fail; * file could not be made, in which case the run only prints. */ QFile g_runLog; +/* Qt's own messages go into the same log as the checks: this is where QML + * puts "TypeError: Cannot read property ... of undefined" and every other + * reason a step did nothing, and a device run has nowhere else to show it + * (logcat does not carry this build's Qt messages -- a plain qInfo from the + * app is not in the buffer). Messages before the log is opened still print. */ +void logQtMessage(QtMsgType type, const QMessageLogContext &, const QString &msg) { + const char *kind = type == QtDebugMsg ? "debug" + : type == QtInfoMsg ? "info" + : type == QtWarningMsg ? "warn" + : type == QtCriticalMsg ? "crit" + : "fatal"; + const QByteArray line = QByteArray(kind) + " " + msg.toUtf8() + "\n"; + if (g_runLog.isOpen()) { + g_runLog.write(line); + g_runLog.flush(); + } + fwrite(line.constData(), 1, static_cast(line.size()), stderr); + if (type == QtFatalMsg) + abort(); +} + void check(bool ok, const QString &what) { std::printf("%s %s\n", ok ? "ok " : "FAIL", qPrintable(what)); std::fflush(stdout); @@ -262,6 +284,7 @@ int runUiTest(AgentCore &core, const QString &size) { * makes, so a device run can be read back without logcat (see g_runLog). */ g_runLog.setFileName(core.workspace() + QStringLiteral("/uitest.log")); g_runLog.open(QIODevice::WriteOnly | QIODevice::Truncate); + qInstallMessageHandler(logQtMessage); const QString resumed = writeFixtureSession(core.workspace(), QStringLiteral("20260101-115959-on")); check(!resumed.isEmpty() && core.switchSession(resumed), @@ -642,6 +665,29 @@ int runUiTest(AgentCore &core, const QString &size) { clickMouse(win, QStringLiteral("settingsButton")); QQuickItem *panel = item(win, QStringLiteral("settingsPanel")); check(panel && panel->isVisible(), QStringLiteral("the gear opens the settings dialog")); + /* item() answers with an item that exists even when nothing is visible (the + * QObject-tree fallback), so "the dialog is not open" is the invisible case + * too. */ + if (!panel || !panel->isVisible()) { + /* Two ways to be wrong -- a click that never lands, and a handler that + * does nothing -- and from the outside they look the same (this failed + * only on the phone, with no QML error at all). Say which, and go on with + * the dialog either way, so the checks below still say something. */ + QQuickItem *gear = item(win, QStringLiteral("settingsButton")); + check(gear != nullptr && gear->isVisible(), + QStringLiteral("... the gear is %1, centre %2,%3 of %4x%5") + .arg(gear && gear->isVisible() ? QStringLiteral("reachable") + : QStringLiteral("not there")) + .arg(gear ? centre(gear).x() : -1) + .arg(gear ? centre(gear).y() : -1) + .arg(win->width()) + .arg(win->height())); + QMetaObject::invokeMethod(win, "openSettings"); + QTest::qWait(120); + panel = item(win, QStringLiteral("settingsPanel")); + check(panel && panel->isVisible(), + QStringLiteral("... and openSettings() opens it when called directly")); + } check(countNamed(win->contentItem(), QStringLiteral("providerRow:")) > 0, QStringLiteral("the dialog lists the providers")); maybeShot(win, QStringLiteral("-settings"));