ZaStoGram_desktop/Telegram/SourceFiles/test/test_window_exposure.h
John Preston e3d790a877 [ai] Share the harness console-lock reading with macOS
Task: 2026/10/08/share-a-console-lock-reading-that-sees-a-lock-taken-before-launch
2026-10-09 23:29:29 +04:00

172 lines
9.3 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 "base/basic_types.h"
#include <QtCore/QPointer>
#include <QtCore/QString>
#include <QtGui/QWindow>
class QWidget;
namespace Test {
class Runner;
// Keeps a top-level window that other applications' windows cover exposed,
// by stacking alone, so its widgets keep painting while nothing activates
// the application, changes the focus window or takes keyboard focus.
//
// Why it exists. 2026/10/07/animate-the-sending-row-for-a-collectible-transfer
// (work/test.md, Attempt 1): Run 1 launched while the owner used the
// console, the launch was not brought to the front, and the Money window
// stayed under the frontmost application - its fixture gate failed with
// pages=1 exposed=0 and every later stage was N/A. On macOS the window
// server reports a fully covered window occluded,
// QCocoaWindow::windowDidChangeOcclusionState calls
// handleExposeEvent(QRegion()) (both in qcocoawindow.mm),
// QWindow::isExposed() turns false, QWidgetWindow::handleExposeEvent clears
// Qt::WA_Mapped (qwidgetwindow.cpp), and the repaint manager
// holds every sync while the window is unmapped
// (QWidgetRepaintManager::sync()): no paint, so no post-paint sample,
// no paint-stamped product timer and no frame advance. Run 2 set the hint
// below on the panel's QWindow right after showing it, waited for
// isExposed() in a pure until, and the leg ran end to end. Since then a
// default workspace.py test-run launch of an app bundle on macOS does not
// activate the client (launch_method "background" in its report,
// --activate to opt in), so its windows open behind the frontmost
// application's whether or not anyone uses the console. A window covered
// from the moment it is shown reads isExposed() true until the window
// server first reports an occlusion change for it - such as this helper's
// undo dropping it back behind - so exposed=1 is no proof that it is on
// screen; after such a change a covered window reads unexposed and gets no
// paints. A stage that needs paints calls the helper either way; the
// workspace.py test-run --wait-idle gate does not change that.
//
// The mechanism. QWindow::setFlag(Qt::WindowStaysOnTopHint, true) on
// widget->window()->windowHandle(). On a shown macOS window
// QCocoaWindow::setWindowFlags re-applies the style mask and the window
// level - NSModalPanelWindowLevel, above every application's ordinary
// windows - and calls no ordering, key-window or activation API; product
// makes the same call on the shown call window
// (Calls::Window::setPinnedOnTop). Banned here, and in every caller that
// relies on this to stay off the owner's keyboard:
// QWidget::setWindowFlag(s) on a shown widget (it re-parents and hides the
// window, QWidgetPrivate::setWindowFlags), raise() (QCocoaWindow::raise orders
// front and calls [NSApp activateIgnoringOtherApps:]), activateWindow(),
// QWindow::requestActivate() and Window::Controller::activate() - an
// activated test client would receive the owner's keystrokes.
//
// The reading. ReadWindowExposure only reads, so it is legal in a stage's
// pure until. In the value KeepWindowExposed returns, |exposed|, |active|
// and |stayOnTop| are the values from before the call, and |applied| is
// true only when this call set the hint. |applicationState|, |focusWindow|
// and |focusIdentity| are the process-wide readings, filled for a refused
// reading too, so a caller compares them across the call to prove that
// nothing was activated. WindowExposureText prints it as one row, with
// stacking=macos|none saying whether this platform has the mechanism.
//
// Refusals are returned, never logged, and the first token of |refusal|
// names them, checked in this order:
// - null-widget: no widget was given;
// - minimized: stacking cannot expose a minimized window, and
// Test::KeepMainWindowNotMarkingRead minimizes the primary window on
// purpose, so the two levers exclude each other;
// - hidden: the widget's own window carries no QWindow handle - it is
// never retargeted at some other window -, or it isHidden(), or its
// QWindow is not visible, for example under Qt::WA_DontShowOnScreen.
//
// It never waits. Exposure arrives asynchronously from the window server,
// so the caller calls the helper, waits for ReadWindowExposure(...).exposed
// in a pure until, and only then asks for the repaint it needs with
// update(): a re-exposed window with nothing dirty is only flushed, not
// painted (the exposed-region QWidgetRepaintManager::sync overload).
//
// The undo. RestoreWindowExposure clears the hint only when that reading
// applied it: a call that found the hint already set applies nothing, so
// its undo leaves the product's own hint alone. It is idempotent and safe
// after the window is gone. Undo before teardown and before reading
// anything the product derives from the hint - Calls::Window::pinnedOnTop()
// reads handle->flags(). QWidget::windowFlags() never carries the helper's
// hint, and a later QWidget::create() re-applies the widget's own flags to
// a fresh QWindow, so a recreated native window loses it. While applied the
// window floats above every application's ordinary windows, where a click
// on it would activate the application, so keep it no longer than the
// paints are needed.
//
// Off macOS it is a documented no-op that still returns its reading
// (applied=0, stacking=none), decided by source inspection. Windows and X11
// never turn a covered window unexposed - qwindowswindow.cpp clears its
// Exposed flag only on hide, and the xcb plugin has no occlusion handling -
// so there is nothing to repair, while the flag would restyle the frame
// there (applyWindowFlags and SWP_FRAMECHANGED) or schedule a native
// window recreation at the next show (qxcbwindow.cpp). Wayland marks a
// window whose frame callbacks stop unexposed, but xdg-shell gives clients
// no stacking control, so the helper cannot repair it there.
// RestoreWindowExposure changes nothing off macOS either.
//
// Marking read. isExposed() is a term of MainWindow::markingAsRead()
// (mainwindow.cpp), so on an unlocked console exposing the primary
// window with auto-scroll-inactive-chat on can make it mark messages read
// (see test_marking_read.h).
struct WindowExposure {
QPointer<QWindow> window;
QString identity;
bool exposed = false;
bool active = false;
bool stayOnTop = false;
bool applied = false;
Qt::ApplicationState applicationState = Qt::ApplicationInactive;
QPointer<QWindow> focusWindow;
QString focusIdentity;
QString refusal;
};
[[nodiscard]] WindowExposure ReadWindowExposure(QWidget *widget);
[[nodiscard]] WindowExposure KeepWindowExposed(QWidget *widget);
void RestoreWindowExposure(const WindowExposure &reading);
[[nodiscard]] QString WindowExposureText(const WindowExposure &reading);
// Needs no session, network or account, and never touches the primary
// window. Two legs - a parentless top-level Ui::RpWidget, a titled window
// at the normal level like the main window, and a Ui::SeparatePanel with
// its stay-on-top hint cleared before its first show - each shown with
// Qt::WA_ShowWithoutActivating and never raised, left by a settle stage
// until it has been shown for 2000 ms, then covered by a harness-owned
// opaque titled top-level window that accepts no focus - a frameless Qt
// top level was measured not to occlude it on macOS. The settle is there
// because a freshly shown window gets one more layer display about half a
// second after it is shown, and Qt counts any layer display as an expose,
// so a target covered sooner reads isExposed() true while the window
// server still reports it occluded; the self-test therefore covers the
// target only after it has settled.
// The control must read the target unexposed under the cover within
// 3000 ms before the helper, or the leg writes a named N/A, never a pass;
// the premise proves the covered target, which nothing asks to repaint,
// stays unexposed and gets no paint for 500 ms; the helper must expose it
// within 3000 ms with no repaint requested, with isActive(), the focus
// window and the application state unchanged and the widget's own flags
// untouched; only then does the self-test ask for one update(), which must
// deliver at least one paint within 3000 ms; the undo must clear the hint
// it set and leave a hint it found alone. Off macOS, or on a console
// Test::ReadConsoleLock() reads locked (test_console_lock.h; on macOS also
// a console locked before the launch, where a window shown behind the lock
// screen keeps a stale isExposed()), every leg stage is a named N/A - the
// lock's N/A prints the CONSOLE_LOCK row, and the control's screenLocked=
// is the same reading - while the refusal stage runs everywhere.
// After a default workspace.py test-run launch, which does not activate the
// client, a target that another application's window covers from the
// moment it is shown cannot be turned unexposed by the harness cover (see
// "Why it exists" above), so that leg is the named N/A, never a pass.
// A per-leg teardown stage and Runner::onFinish, registered at append time,
// destroy every harness window, so none is left over the owner's console on
// any exit path.
void AppendKeepWindowExposedSelfTest(not_null<Runner*> runner);
} // namespace Test