[ai] Capture off-screen rows and name misframing

Task: 2026/07/28/fix-blank-row-captures-in-test-harness
This commit is contained in:
John Preston
2026-07-29 00:09:57 +04:00
parent 4911100c2d
commit f1e45d45eb
2 changed files with 65 additions and 15 deletions

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "test/test_capture.h"
#include "test/test_log.h"
#include "ui/ui_utility.h"
#include <QtGui/QPainter>
@@ -23,6 +24,31 @@ constexpr auto kContactSheetGap = 8;
: (name + u".png"_q);
}
[[nodiscard]] QString RectText(const QRect &rect) {
return u"%1,%2 %3x%4"_q
.arg(rect.x())
.arg(rect.y())
.arg(rect.width())
.arg(rect.height());
}
[[nodiscard]] QString MisframedDetails(
not_null<QWidget*> widget,
const QRect &logicalRect) {
const auto bounds = widget->rect();
if (!logicalRect.isEmpty() && bounds.contains(logicalRect)) {
return QString();
}
const auto inside = bounds.intersected(logicalRect);
return u"requested rect is not fully inside the grabbed widget: "
u"requested=%1 widget=%2 inside=%3 rows=%4/%5 columns=%6/%7"_q
.arg(RectText(logicalRect), RectText(bounds), RectText(inside))
.arg(inside.height())
.arg(logicalRect.height())
.arg(inside.width())
.arg(logicalRect.width());
}
} // namespace
QImage GrabWidget(not_null<QWidget*> widget) {
@@ -32,13 +58,10 @@ QImage GrabWidget(not_null<QWidget*> widget) {
QImage GrabRect(
not_null<QWidget*> widget,
const QRect &logicalRect) {
const auto image = GrabWidget(widget);
const auto ratio = image.devicePixelRatio();
return Crop(image, QRect(
int(std::floor(logicalRect.x() * ratio)),
int(std::floor(logicalRect.y() * ratio)),
int(std::ceil(logicalRect.width() * ratio)),
int(std::ceil(logicalRect.height() * ratio))));
const auto bounded = logicalRect.intersected(widget->rect());
return bounded.isEmpty()
? QImage()
: widget->grab(bounded).toImage();
}
bool LooksBlank(const QImage &image) {
@@ -75,6 +98,7 @@ QString SaveImage(const QImage &image, const QString &name) {
}
bool CaptureWidget(not_null<QWidget*> widget, const QString &name) {
LogGeometry(name, QRect(widget->mapToGlobal(QPoint()), widget->size()));
if (!widget->isVisible()) {
Fail(u"capture %1"_q.arg(name), u"widget is not visible"_q);
return false;
@@ -84,7 +108,6 @@ bool CaptureWidget(not_null<QWidget*> widget, const QString &name) {
Fail(u"capture %1"_q.arg(name), u"grabbed image looks blank"_q);
return false;
}
LogGeometry(name, QRect(widget->mapToGlobal(QPoint()), widget->size()));
return !SaveImage(image, name).isEmpty();
}
@@ -92,13 +115,14 @@ bool CaptureRect(
not_null<QWidget*> widget,
const QRect &logicalRect,
const QString &name) {
LogGeometry(name, logicalRect);
if (!widget->isVisible()) {
Fail(u"capture %1"_q.arg(name), u"widget is not visible"_q);
return false;
} else if (!QRect(QPoint(), widget->size()).intersects(logicalRect)) {
Fail(
u"capture %1"_q.arg(name),
u"rect is outside the widget bounds"_q);
}
const auto misframed = MisframedDetails(widget, logicalRect);
if (!misframed.isEmpty()) {
Fail(u"capture %1"_q.arg(name), misframed);
return false;
}
const auto image = GrabRect(widget, logicalRect);
@@ -106,10 +130,20 @@ bool CaptureRect(
Fail(u"capture %1"_q.arg(name), u"grabbed image looks blank"_q);
return false;
}
LogGeometry(name, logicalRect);
return !SaveImage(image, name).isEmpty();
}
bool CaptureMappedRect(
not_null<QWidget*> widget,
not_null<QWidget*> rectOrigin,
const QRect &logicalRect,
const QString &name) {
return CaptureRect(
widget,
Ui::MapFrom(widget, rectOrigin, logicalRect),
name);
}
QImage Crop(const QImage &image, const QRect &pixelRect) {
const auto bounded = pixelRect.intersected(image.rect());
return bounded.isEmpty() ? QImage() : image.copy(bounded);

View File

@@ -15,8 +15,10 @@ namespace Test {
// elements, other windows, or a locked desktop session.
[[nodiscard]] QImage GrabWidget(not_null<QWidget*> widget);
// Grabs the widget and crops to |logicalRect| (widget-local logical
// coordinates), handling device pixel ratio.
// Renders only |logicalRect| (widget-local logical coordinates) out of the
// widget, clamped to its bounds, handling device pixel ratio. The grab
// renders the widget itself, so a rect outside the visible area is still
// produced.
[[nodiscard]] QImage GrabRect(
not_null<QWidget*> widget,
const QRect &logicalRect);
@@ -30,12 +32,26 @@ QString SaveImage(const QImage &image, const QString &name);
// Grab + blank-check + save as one evidence-grade capture: a hidden widget,
// an empty grab, or a blank image is a logged FAIL, never a silent pass.
// CaptureRect's rect must lie fully inside the grabbed widget; a rect that
// leaves it is a logged FAIL naming the overlap, never a silently reframed
// image.
bool CaptureWidget(not_null<QWidget*> widget, const QString &name);
bool CaptureRect(
not_null<QWidget*> widget,
const QRect &logicalRect,
const QString &name);
// Captures |logicalRect|, expressed in |rectOrigin| coordinates, by grabbing
// |widget| — the rect is mapped into |widget| first. A grab renders the
// widget itself, so a row scrolled out of view is captured by passing the
// scrolled content widget as |widget|; the window that clips it holds no
// pixels for that row.
bool CaptureMappedRect(
not_null<QWidget*> widget,
not_null<QWidget*> rectOrigin,
const QRect &logicalRect,
const QString &name);
[[nodiscard]] QImage Crop(const QImage &image, const QRect &pixelRect);
// Nearest-neighbor upscale for readable small-target evidence.