uitest: scroll a card into view before clicking it

Open and Share failed on the phone for a reason that was in the harness,
not in the app: the card was where isVisible() says yes, but the transcript
clips it away -- the run's own note says it found contentY at -14 with the
card's row below the visible part -- so the click (and the tap after it)
landed on whatever was at that point instead, and the header stayed empty.

The run now stops the view following its tail (ChatView.stickBottom, which
the replay sets), scrolls the card's row into view with the numbers in a
note, and only then clicks. ChatView gets an objectName for that.

The checks also say what the card hands the engine (the name the store
keeps the file under, from the strip's own model) and what the card itself
answers -- which is what turned "it did nothing" into this answer:

  note the card is out of the viewport (contentY -14, card at 433 of 615):
       scrolling to it
  ok   Open hands the file to the system (opened report.md (test mode);
       cards hold 'img-...png, art-....md', on screen, card says '')
  ok   Share names the file (shared report.md (test mode); card says '')

The whole device run is green now: 97 ok, 0 FAIL (it was 88/17 when this
session's work started, and every one of those was real).
This commit is contained in:
WHC
2026-10-11 21:14:54 +08:00
parent a99f78134a
commit 81aeee7d1e
2 changed files with 72 additions and 9 deletions
+1
View File
@@ -374,6 +374,7 @@ ApplicationWindow {
ChatView { ChatView {
id: chat id: chat
objectName: "chatView"
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
model: transcript model: transcript
+71 -9
View File
@@ -146,6 +146,27 @@ QPoint centre(QQuickItem *it) {
return p.toPoint(); return p.toPoint();
} }
/* Is this item's centre really reachable? isVisible() answers whether hiding
* was asked for, not whether a clipping ancestor (a scrolling view) has moved
* it out of sight -- and an event goes to whatever is at that point instead. */
bool onScreen(QQuickItem *it) {
if (!it || !it->isVisible() || it->width() <= 0 || it->height() <= 0)
return false;
QQuickWindow *win = it->window();
if (!win)
return false;
const QPointF p = it->mapToScene(QPointF(it->width() / 2, it->height() / 2));
if (!QRectF(0, 0, win->width(), win->height()).contains(p))
return false;
for (QQuickItem *a = it->parentItem(); a; a = a->parentItem()) {
if (!a->clip())
continue;
if (!a->mapRectToScene(QRectF(0, 0, a->width(), a->height())).contains(p))
return false;
}
return true;
}
void clickMouse(QQuickWindow *win, const QString &name) { void clickMouse(QQuickWindow *win, const QString &name) {
QQuickItem *it = item(win, name); QQuickItem *it = item(win, name);
if (!it || !it->isVisible()) { if (!it || !it->isVisible()) {
@@ -437,19 +458,59 @@ int runUiTest(AgentCore &core, const QString &size) {
item(win, QStringLiteral("fileShare0")) != nullptr, item(win, QStringLiteral("fileShare0")) != nullptr,
QStringLiteral("the card carries open and share")); QStringLiteral("the card carries open and share"));
win->setProperty("statusText", QString()); win->setProperty("statusText", QString());
/* A click goes to whatever is at that point, and the transcript replays
* from its bottom: on a phone-sized window the card can be above the
* visible part, where isVisible() still says yes (it does not know about
* the view's clipping) and the click lands on the row that happens to be
* there. Scroll it in first, which is what a person does -- and stop the
* view following its tail (ChatView.stickBottom), or the scroll is undone
* by the next content change. */
if (list && !onScreen(item(win, QStringLiteral("fileOpen0")))) {
QQuickItem *button = item(win, QStringLiteral("fileOpen0"));
QQuickItem *chat = item(win, QStringLiteral("chatView"));
if (chat)
chat->setProperty("stickBottom", false);
const double contentY = list->property("contentY").toDouble();
const double y = button->mapToItem(list, QPointF(0, 0)).y() + contentY;
const double lowest =
list->property("contentHeight").toDouble() - list->height();
note(QStringLiteral("the card is out of the viewport (contentY %1, card "
"at %2 of %3): scrolling to it")
.arg(contentY)
.arg(y)
.arg(list->property("contentHeight").toDouble()));
list->setProperty("contentY", qBound(0.0, y - list->height() / 2, lowest));
QTest::qWait(150);
}
clickMouse(win, QStringLiteral("fileOpen0")); clickMouse(win, QStringLiteral("fileOpen0"));
/* This button lives in the scrolling transcript, and on the NX789J the /* What the card itself hands the engine ("rel" is the name the store keeps
* synthesized mouse click does not reach a button there -- the same click * the file under), and whether it is really on screen: a card outside the
* lands in the header and in the sidebar. What a phone has is a finger, so * viewport takes the events meant for it, and the card's own line is where
* try that too, and say in the message which one it took: neither is a * the engine puts its refusal. */
* failure to hide. */ QStringList rels;
QString cardSays;
QList<QQuickItem *> strips;
collectNamed(win->contentItem(), QStringLiteral("imageStrip"), &strips);
for (QQuickItem *s : strips) {
const QVariantList files = s->property("files").toList();
for (const QVariant &f : files)
rels << f.toMap().value(QStringLiteral("rel")).toString();
if (cardSays.isEmpty())
cardSays = s->property("errorText").toString();
}
const bool byMouse = !win->property("statusText").toString().isEmpty(); const bool byMouse = !win->property("statusText").toString().isEmpty();
if (!byMouse) if (!byMouse)
tapTouch(win, touch, QStringLiteral("fileOpen0")); tapTouch(win, touch, QStringLiteral("fileOpen0"));
const QString opened = win->property("statusText").toString(); const QString opened = win->property("statusText").toString();
check(opened.startsWith(QLatin1String("opened ")), check(opened.startsWith(QLatin1String("opened ")),
QStringLiteral("Open hands the file to the system (%1%2)") QStringLiteral("Open hands the file to the system (%1%2; cards hold '%3', "
.arg(opened, byMouse ? QString() : QStringLiteral(" after a tap"))); "%4, card says '%5')")
.arg(opened, byMouse ? QString() : QStringLiteral(" after a tap"),
rels.join(QLatin1String(", ")),
onScreen(item(win, QStringLiteral("fileOpen0")))
? QStringLiteral("on screen")
: QStringLiteral("out of the viewport"),
cardSays));
win->setProperty("statusText", QString()); win->setProperty("statusText", QString());
clickMouse(win, QStringLiteral("fileShare0")); clickMouse(win, QStringLiteral("fileShare0"));
const bool shareByMouse = !win->property("statusText").toString().isEmpty(); const bool shareByMouse = !win->property("statusText").toString().isEmpty();
@@ -457,8 +518,9 @@ int runUiTest(AgentCore &core, const QString &size) {
tapTouch(win, touch, QStringLiteral("fileShare0")); tapTouch(win, touch, QStringLiteral("fileShare0"));
const QString shared = win->property("statusText").toString(); const QString shared = win->property("statusText").toString();
check(shared.contains(QLatin1String("report.md")), check(shared.contains(QLatin1String("report.md")),
QStringLiteral("Share names the file (%1%2)") QStringLiteral("Share names the file (%1%2; card says '%3')")
.arg(shared, shareByMouse ? QString() : QStringLiteral(" after a tap"))); .arg(shared, shareByMouse ? QString() : QStringLiteral(" after a tap"),
cardSays));
const QString refused = const QString refused =
core.openArtifact(QStringLiteral("art-not-one-of-ours.md")); core.openArtifact(QStringLiteral("art-not-one-of-ours.md"));
check(refused.contains(QLatin1String("no such file")), check(refused.contains(QLatin1String("no such file")),