ZaStoGram_desktop/Telegram/SourceFiles/test/test_toast_capture.h
John Preston be18582871 [ai] Record live-toast ambiguity and expiry in harness
Task: 2026/09/07/record-live-toast-ambiguity-and-expiry-in-the-harness
2026-09-24 22:53:08 +04:00

202 lines
11 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "test/test_runner.h"
#include <QtCore/QPointer>
#include <QtGui/QImage>
#include <QtWidgets/QWidget>
#include <vector>
namespace Test {
// The one capture for a toast, and the only one whose saved frame cannot
// hold the product surface the toast was drawn over.
//
// Test::CaptureViaWindow grabs the toast's window cropped to the toast's
// rect mapped into it. That is the right answer to blankness and the wrong
// answer to hygiene. st::toastBg is #2c3033e5 (ui/colors.palette:448) -
// alpha 0xE5, about 90% opaque - so roughly a tenth of every pixel the
// product painted underneath composes into the saved frame, legibly enough
// to read text through it. The rect's corners are worse than that:
// Widget::paintBackground is _roundRect.paint(p, rect())
// (ui/toast/toast_widget.cpp:525-528), which fills the straight bands and
// draws four corner images (ui/round_rect.cpp:65-125) whose mask is filled
// with Qt::transparent and carries only a rounded rect
// (ui/image/image_prepare.cpp:75-98), so outside the arc the toast paints
// nothing at all and whatever is behind it shows there unblended.
//
// Test::GrabWidget and Test::GrabRect call Ui::GrabWidgetToImage(widget,
// rect, st::windowBg->c) (test_capture.cpp:236-247), which fills the result
// with the harness theme base and then renders only that target's own
// subtree (ui/ui_utility.cpp:133-151). A grab rooted at the toast is
// therefore hygienic by construction: every pixel the translucent toast
// leaves uncovered measures st::windowBg and never the product. That is
// the whole mechanism this module rests on, and it is why a root that is
// not the live toast is refused here rather than reframed.
//
// Ui::Toast::internal::Widget::_shownLevel is private (toast_widget.h:40),
// and while it is below 1 paintEvent draws the whole frame into a
// transparent proxy at that opacity and returns (toast_widget.cpp:585-600)
// - which is exactly where a toast-rooted grab reads blank. So settledness
// here is measured and not read: the settled background blend is derived
// from the live palette and one grab is sampled against it.
//
// Ui::Toast::internal::Manager exposes only instance() and addToast() and
// keeps its _toastByWidget map private (ui/toast/toast_manager.h:27-29,
// :42), so no API enumerates live toasts and the honest resolution is the
// harness's own visible-widget walk over the top level widgets.
//
// This is its own module rather than part of test_capture.h because it
// appends Runner stages and test_runner.cpp already includes
// test_capture.h: the reverse include would invert the harness's layering
// and make its most-included module runner-aware, which is the rule
// test_menu.h:57-63 states. It is not part of test_via_window.* either,
// because that module is the self-test of CaptureViaWindow and is this
// module's own unchanged-scenario control.
// |toast| is non-null exactly when |refusal| is empty at the moment the
// reading is taken: a caller cannot take the pointer without being handed
// the reason there is none. It is a QPointer<QWidget>, so a retained
// reading can only go from resolved to unresolved and never hands back a
// pointer into freed memory, while |frame|, |identity| and |refusal| are
// values that stay printable after the toast is gone.
// Test::WindowMappedCapture (test_capture.h:199-208) and
// Test::PaintingLayerRootResult (:142-149) carry the same type and the
// same contract.
struct ToastSubtreeReading {
QPointer<QWidget> toast;
QRect frame;
QString refusal;
QString identity;
[[nodiscard]] bool resolved() const {
return toast != nullptr;
}
};
// The visible-widget walk that answers which toasts are live now. It runs
// over every top level widget rather than one root, because a toast is
// parented wherever Ui::Toast::Show was called. FindLiveToast() answers
// that single toast, and nullptr when the walk finds zero of them or more
// than one - an ambiguous answer is refused rather than guessed.
[[nodiscard]] std::vector<QWidget*> FindLiveToasts();
[[nodiscard]] QWidget *FindLiveToast();
// ReadToastSubtree takes no grab. A root that is not a live
// Ui::Toast::internal::Widget is refused by name, and so is a child inside
// a toast, on purpose: the frame is rooted at the toast or nowhere,
// because everything above it paints the product and everything below it
// is not the toast. A non-empty |requested| that the toast's own rect does
// not contain is refused quoting both rects, never silently reframed.
[[nodiscard]] ToastSubtreeReading ReadToastSubtree(
QWidget *widget,
QRect requested = QRect());
// GrabToastSubtree, ToastSubtreeReady and ToastSubtreeDetails take exactly
// one grab each, so a poll costs one grab per tick - the budget
// test_capture.h:185-187 promises for the window-mapped family. A refused
// request answers a null image and never a reframed one, so a poll around
// a refused target ends in a named stage timeout rather than in a grab of
// something else. ToastSubtreeReady is the settled-show term: it is true
// once the sampled frame matches st::toastBg's own settled blend over the
// harness base, which no mid-fade frame does.
[[nodiscard]] QImage GrabToastSubtree(
QWidget *widget,
QRect requested = QRect());
[[nodiscard]] bool ToastSubtreeReady(QWidget *widget);
[[nodiscard]] QString ToastSubtreeDetails(QWidget *widget);
// Loud on a structural refusal, because no amount of waiting repairs one:
// no widget, a root that is not the live toast, an invisible or empty
// target, or a rect outside the toast's geometry. A Note and never a FAIL
// on an unsettled or blank frame, because the decisive oracle for a toast
// is textual and this capture corroborates it.
bool CaptureToastSubtree(
not_null<QWidget*> widget,
const QString &name,
QRect requested = QRect());
// The joined accessibilityName() of the toast's Ui::FlatLabels - the read
// the harness README already calls decisive - and its comparison, which
// goes through the shipped Test::CheckTextReads (test_text_reads.h:31-34)
// so that the space-class normalization and the both-verdicts printing are
// composed rather than re-derived. A widget that is not a live toast reads
// back an empty string.
[[nodiscard]] QString ReadToastText(QWidget *widget);
void CheckToastReads(
QWidget *widget,
const QString &expected,
const QString &what);
// This module measuring itself, in seven stages, over a synthetic
// sentinel-bearing surface it paints and a real Ui::Toast of its own.
//
// The surface is two horizontal bands of two high-contrast tones chosen so
// that nothing a toast-rooted frame can hold comes near either of them on
// any palette, and not merely on the one the tones were picked against.
// Such a frame carries st::windowBg, blends of st::toastBg over it and
// st::toastFg label ink, every one of which is a grey or within a few
// channel units of one, as is every antialiased mixture of them; and
// Test::ChannelDelta is the maximum absolute per-channel difference, so
// the nearest grey to a colour sits exactly half that colour's own channel
// span away. Both tones span nearly the whole range, which puts every grey
// at least 95 channel units from either and every near-grey at least about
// 92 - far outside the counter's 40-unit tolerance, whether the theme is
// light or dark. Stage 1 measures that instead of trusting it: a named
// fixture gate reports the separation from st::windowBg, from the settled
// toast blend and from st::toastFg, and FAILs if the smallest of them
// drops below twice the tolerance, so a palette that ever breaks the
// argument says so in the log rather than flipping the counter's verdict
// on a correct frame. The two tones also keep a window-mapped crop of the
// same rect clear of the blank threshold without resting on the toast's
// own paint - their lightness is 160 against 96 - which is the reason
// test_via_window.cpp:65-68 gives for using two. No account fixture
// secret, wallet phrase, password or hint is read, painted or named
// anywhere: the only text the self-test paints is its own literal.
//
// Stage 1 reads the fade-in window in the one turn it exists, where the
// shown level is still 0, and shows the settled-show term answering false
// there and the capture declining with a Note that moves no failure count.
// Stage 2 waits on that same term as a real poll and then decides the
// module's claim twice: once by counting sentinel pixels in the two saved
// PNGs of the same toast in the same turn, and once - strictly stronger
// and independent of the corners - by hiding the surface beneath and
// showing that no pixel of the subtree frame moved while many pixels of
// the window-mapped frame did. It also reads the toast's phrase back and
// shows the same comparison declining a different phrase. Stage 3 shows a
// second toast beside the fixture and takes it down inside the same turn,
// where the walk answers two and FindLiveToast() refuses the ambiguity
// with nullptr; the fixture is shown single again there and a tick later.
// Stages 4 and 5 show a non-infinite toast with kDefaultDuration, wait on
// ToastSubtreeReady inside its lifetime, then wait until the product's
// own hide path has taken it out of the walk - quoting the live count,
// the texts and the elapsed time - so the fixture is single again before
// Stage 6 reads. Stage 6 refuses a null, an ancestor that paints the
// product and a rect larger than the toast, each by name and quoting both
// rects. Stage 7 is teardown, and it is where the other refusal is read:
// the control walk answers the fixture toast alone, this module's own
// Instance::hide() takes it down, and the same walk then answers an empty
// list that FindLiveToast() refuses with nullptr.
//
// It needs no session, no chats list, no network and no account fixture.
// The only thing it asks of the process is a primary window to parent the
// fixture to, and a missing one is reported as a named fixture gate
// instead of crashing. It appends its own teardown last, and it emits no
// deliberate failure: every refusal it demonstrates is observed through
// the pure FindLiveToasts, FindLiveToast, ReadToastSubtree,
// ToastSubtreeReady and ToastSubtreeDetails readings, which log nothing,
// and asserted as a passing Check whose details carry the refusal
// verbatim. test_text_reads.h:70-78 records that module as the harness's
// one self-test that emits deliberate failures, and this one does not
// become a second.
void AppendToastSubtreeCaptureSelfTest(not_null<Runner*> runner);
} // namespace Test