File: TrayApplication.NativeSmoke.cs
Web Access
Project: src\src\Aspire.Tray\Windows\Aspire.Tray.Windows.csproj (aspire-tray)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.InteropServices;
 
namespace Aspire.Tray;
 
// Smoke hooks drive the real flyout: clicks and keys go through its window procedure as
// WM_LBUTTONDOWN/UP and WM_KEYDOWN, presses through the same Invoke path UI Automation uses,
// and actions through the panel host. No browser or application is launched.
internal sealed unsafe partial class TrayApplication
{
    private SmokeDialog? _smokeDialog;
 
    internal nint IconForSmoke => _iconData.Icon;
    internal int IconAddFailuresForSmoke { get; set; }
 
    private void ProbeRegistrationForSmoke()
    {
        var data = new NativeMethods.NotifyIconData
        {
            Size = (uint)sizeof(NativeMethods.NotifyIconData),
            Window = _window,
            Id = 2,
            Flags = _iconData.Flags,
            CallbackMessage = _iconData.CallbackMessage,
            // IDI_APPLICATION is shared system artwork; it must not be destroyed.
            Icon = NativeMethods.LoadIcon(0, 32512)
        };
        NativeCallException.Require(data.Icon != 0, "LoadIconW(smoke control)");
        Program.Log($"C# NOTIFYICONDATAW architecture={RuntimeInformation.ProcessArchitecture} size={sizeof(NativeMethods.NotifyIconData)} "
            + $"hwnd={Offset(nameof(data.Window))} id={Offset(nameof(data.Id))} flags={Offset(nameof(data.Flags))} callback={Offset(nameof(data.CallbackMessage))} "
            + $"icon={Offset(nameof(data.Icon))} tip={Offset(nameof(data.Tip))} state={Offset(nameof(data.State))} stateMask={Offset(nameof(data.StateMask))} "
            + $"info={Offset(nameof(data.Info))} version={Offset(nameof(data.Version))} title={Offset(nameof(data.InfoTitle))} "
            + $"infoFlags={Offset(nameof(data.InfoFlags))} guid={Offset(nameof(data.ItemGuid))} balloon={Offset(nameof(data.BalloonIcon))}.");
        var added = NativeMethods.ShellNotifyIcon(NativeMethods.NimAdd, ref data);
        Program.Log($"C# stock icon NIM_ADD={added} flags=0x{data.Flags:x}.");
        if (added != 0)
        {
            NativeCallException.Require(NativeMethods.ShellNotifyIcon(NativeMethods.NimDelete, ref data) != 0, "Shell_NotifyIconW(smoke control delete)");
        }
 
        static nint Offset(string field) => Marshal.OffsetOf<NativeMethods.NotifyIconData>(field);
    }
 
    internal void BeginInteractivePreviewForSmoke()
    {
        RequireSmoke();
        _interactiveSmoke = true;
        _smokeDeadline = null;
        ShowPreviewPanel();
    }
 
    // PNG encoder of GDI+ (ImageCodecInfo.Clsid for image/png).
    private static readonly Guid s_pngEncoder = new("557CF406-1A04-11D3-9A73-0000F81EF32E");
    private static TrayPanelPalette? s_previewPalette;
    private string? _previewExport;
    private long _previewStateAt;
    private long _previewCaptureAt;
 
    /// <summary>
    /// Opens the interactive preview's panel. For alignment screenshots, environment variables
    /// pick its palette (ASPIRE_TRAY_SMOKE_PREVIEW_APPEARANCE: light or dark), its state
    /// (ASPIRE_TRAY_SMOKE_PREVIEW_STATE: hover shows the first row's buttons, actions opens its
    /// action list, settings opens the Settings view), and a JSON file to export the scene to
    /// (ASPIRE_TRAY_SMOKE_PREVIEW_EXPORT). The state is applied a moment after the panel opens,
    /// once the preview's fake AppHosts have arrived, and the scene is exported together with a
    /// PNG of the composited screen under the panel once DWM has presented it, so both describe
    /// the same frame.
    /// </summary>
    private void ShowPreviewPanel()
    {
        s_previewPalette = Environment.GetEnvironmentVariable("ASPIRE_TRAY_SMOKE_PREVIEW_APPEARANCE") switch
        {
            "light" => TrayPanelPalette.Light,
            "dark" => TrayPanelPalette.Dark,
            _ => null
        };
        ShowPanel();
        _previewStateAt = Environment.TickCount64 + 1500;
        _previewExport = Environment.GetEnvironmentVariable("ASPIRE_TRAY_SMOKE_PREVIEW_EXPORT") is { Length: > 0 } export ? export : null;
    }
 
    /// <summary>Applies the preview's state on the timer, after the fake AppHosts have arrived.</summary>
    private void ApplyPreviewState()
    {
        if (_previewStateAt == 0 || Environment.TickCount64 < _previewStateAt || !_panelVisible)
        {
            return;
        }
        _previewStateAt = 0;
        var panel = _panel!;
        var first = panel.Scene.Targets.FirstOrDefault(target => target.Kind == PanelTargetKind.Row);
        switch (Environment.GetEnvironmentVariable("ASPIRE_TRAY_SMOKE_PREVIEW_STATE"))
        {
            case "hover" when first is not null:
                // Straight to the panel, not through WM_MOUSEMOVE: the real pointer is elsewhere,
                // and TrackMouseEvent would report it leaving at once.
                ApplyPanelResponse(panel.PointerMoved(first.HitBounds.X + 40, first.HitBounds.CenterY));
                break;
            case "actions" when first is not null:
                ApplyPanelResponse(panel.Invoke(first.Key with { Kind = PanelTargetKind.More }));
                break;
            case "settings":
                RunPanelAction(new(PanelActionKind.Settings));
                break;
        }
        _previewCaptureAt = Environment.TickCount64 + 700;
    }
 
    /// <summary>
    /// Exports the scene and saves the composited screen under the panel window, as presented,
    /// once the state has been applied and painted.
    /// </summary>
    private void CapturePreviewForSmoke()
    {
        ApplyPreviewState();
        if (_previewExport is not { } export || _previewCaptureAt == 0 || Environment.TickCount64 < _previewCaptureAt || !_panelVisible)
        {
            return;
        }
        _previewExport = null;
        var path = Path.ChangeExtension(export, ".png");
        NativeCallException.Require(NativeMethods.GetWindowRect(_panelWindow, out var rect) != 0, "GetWindowRect(capture)");
        TrayPanelSceneJson.Write(export, _panel!.Scene, "Windows", Environment.GetEnvironmentVariable("ASPIRE_TRAY_SMOKE_PREVIEW_APPEARANCE") ?? "system",
            PanelScale, _panelWindow, (rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top));
        var width = rect.Right - rect.Left;
        var height = rect.Bottom - rect.Top;
        var screen = NativeMethods.GetDC(0);
        NativeCallException.Require(screen != 0, "GetDC(screen)");
        var memory = NativeMethods.CreateCompatibleDC(screen);
        var bitmap = NativeMethods.CreateCompatibleBitmap(screen, width, height);
        try
        {
            var previous = NativeMethods.SelectObject(memory, bitmap);
            // SRCCOPY | CAPTUREBLT: read the composited desktop, including layered windows.
            NativeCallException.Require(NativeMethods.BitBlt(memory, 0, 0, width, height, screen, rect.Left, rect.Top, 0x00CC0020 | 0x40000000) != 0,
                "BitBlt(capture)");
            NativeMethods.SelectObject(memory, previous);
            RequireGdiplus(NativeMethods.GdipCreateBitmapFromHBITMAP(bitmap, 0, out var image), "GdipCreateBitmapFromHBITMAP");
            try
            {
                RequireGdiplus(NativeMethods.GdipSaveImageToFile(image, path, in s_pngEncoder, 0), "GdipSaveImageToFile");
            }
            finally
            {
                NativeMethods.GdipDisposeImage(image);
            }
            Program.Log($"Preview captured to {path}.");
        }
        finally
        {
            Cleanup(NativeMethods.DeleteObject(bitmap) != 0, "DeleteObject(capture)");
            Cleanup(NativeMethods.DeleteDC(memory) != 0, "DeleteDC(capture)");
            NativeMethods.ReleaseDC(0, screen);
        }
    }
 
    internal void ShowMessageForSmoke()
    {
        RequireSmoke();
        ShowMessage("Aspire smoke", "Native message dialog.", 0);
    }
 
    private void CompleteDialogForSmoke()
    {
        if (_smokeDialog is not { } pending)
        {
            return;
        }
        var owner = _modalOwner != 0 ? _modalOwner : _window;
        var dialog = GetEnabledPopup(owner);
        if (dialog == 0 || dialog == owner)
        {
            return;
        }
        // DM_GETDEFID returns MAKELONG(default button ID, DC_HASDEFID). Verify the
        // actual native dialog, then post a button command through its normal modal loop.
        var defaultId = (nuint)NativeMethods.SendMessage(dialog, 0x400, 0, 0);
        NativeSmokeHarness.Require((defaultId & 0xFFFF) == (uint)pending.DefaultId && (defaultId >> 16) == 0x534B,
            "The actual native dialog has an unsafe default button.");
        var button = NativeMethods.GetDlgItem(dialog, pending.Response);
        NativeCallException.Require(button != 0, "GetDlgItem(smoke dialog response)");
        // WM_COMMAND/BN_CLICKED includes the button HWND in lParam.
        // https://learn.microsoft.com/windows/win32/controls/bn-clicked
        NativeCallException.Require(NativeMethods.PostMessage(dialog, NativeMethods.WmCommand, (nuint)pending.Response, button) != 0, "PostMessageW(smoke dialog response)");
        _smokeDialog = null;
    }
 
    /// <summary>Shows the panel when it is hidden (Windows may deactivate it in a background run).</summary>
    private TrayPanel EnsurePanelForSmoke()
    {
        RequireSmoke();
        if (!_panelVisible)
        {
            ShowPanel();
        }
        return _panel!;
    }
 
    internal TrayPanel PanelForSmoke => EnsurePanelForSmoke();
 
    /// <summary>Finds a target, expanding the recent list and scrolling as a user would to reach it.</summary>
    private PanelHitTarget FindPanelTargetForSmoke(PanelTargetKind kind, AppHostId? host, int item = 0)
    {
        var panel = EnsurePanelForSmoke();
        if (host is { } id && !panel.IsRecentExpanded && panel.State.RecentAppHosts.Any(recent => recent.Id == id)
            && !panel.Scene.Targets.Any(target => target.Key.Host == id))
        {
            ClickPanelForSmoke(panel.Scene.Targets.Single(target => target.Kind == PanelTargetKind.ToggleRecent));
        }
        var target = panel.Scene.Targets.FirstOrDefault(target => target.Kind == kind && target.Key.Host == host && target.Key.Item == item)
            ?? throw new InvalidOperationException($"The panel has no {kind} target for the AppHost.");
        if (target.HitBounds.IsEmpty)
        {
            // Scroll it into view as the wheel would. A positive delta reveals content above.
            var viewport = panel.Viewport;
            ApplyPanelResponse(panel.Scroll(target.Bounds.Y < viewport.Y ? viewport.Y - target.Bounds.Y : viewport.Bottom - target.Bounds.Bottom));
            target = panel.Scene.Targets.Single(other => other.Key == target.Key);
        }
        return target;
    }
 
    /// <summary>Clicks a target with real mouse messages sent to the panel's window procedure.</summary>
    internal void ClickPanelForSmoke(PanelTargetKind kind, AppHostId? host = null, int item = 0)
        => ClickPanelForSmoke(FindPanelTargetForSmoke(kind, host, item));
 
    private void ClickPanelForSmoke(PanelHitTarget target)
    {
        // Client coordinates in device pixels, packed as MAKELPARAM(x, y).
        var x = (int)Math.Round(target.HitBounds.CenterX * PanelScale);
        var y = (int)Math.Round(target.HitBounds.CenterY * PanelScale);
        var point = (nint)(uint)((ushort)x | ((ushort)y << 16));
        NativeMethods.SendMessage(_panelWindow, NativeMethods.WmMouseMove, 0, point);
        NativeMethods.SendMessage(_panelWindow, NativeMethods.WmLeftButtonDown, 1, point);
        NativeMethods.SendMessage(_panelWindow, NativeMethods.WmLeftButtonUp, 0, point);
    }
 
    /// <summary>Presses a target the way UI Automation's Invoke does.</summary>
    internal void PressPanelForSmoke(PanelTargetKind kind, AppHostId? host = null)
        => ApplyPanelResponse(_panel!.Invoke(FindPanelTargetForSmoke(kind, host).Key));
 
    /// <summary>Sends a key to the panel's window procedure.</summary>
    internal void SendPanelKeyForSmoke(int virtualKey)
    {
        EnsurePanelForSmoke();
        NativeMethods.SendMessage(_panelWindow, NativeMethods.WmKeyDown, (nuint)virtualKey, 0);
    }
 
    /// <summary>Opens a row's action list and picks one of its items.</summary>
    internal void PickRowActionForSmoke(AppHostId host, PanelTargetKind kind, int item = 0)
    {
        ClickPanelForSmoke(PanelTargetKind.More, host);
        ClickPanelForSmoke(kind, host, item);
        NativeSmokeHarness.Require(_panel!.ActionsRow is null, "Picking an action must close the action list.");
    }
 
    /// <summary>
    /// Opens a row's action list through UI Automation, checks its items and their order, moves
    /// into it with the keyboard, and closes it with Escape.
    /// </summary>
    internal void VerifyActionListForSmoke(AppHostId host)
    {
        PressPanelForSmoke(PanelTargetKind.More, host);
        var panel = _panel!;
        var row = panel.Scene.Targets.Single(target => target.Kind == PanelTargetKind.Row && target.Key.Host == host);
        var name = row.Host!.DisplayName;
        var expected = (_installedApplications.Length > 0 ? [$"Open {name} in…"] : Array.Empty<string>())
            .Concat([$"Show {name} in File Explorer", $"Copy the folder path of {name}", row.Host.IsPinned ? $"Unpin {name}" : $"Pin {name}"])
            .Concat(row.Key.Section == PanelSection.Recent ? [$"Remove {name} from recent AppHosts"] : []);
        var items = panel.Scene.Targets.Where(target => target.Key.Host == host && target.Bounds.Y > row.Bounds.Bottom).ToList();
        NativeSmokeHarness.Require(panel.ActionsRow == (row.Key.Section, host) && items.Select(target => target.AccessibleName).SequenceEqual(expected),
            $"The action list does not offer the expected items: {string.Join(", ", items.Select(target => target.AccessibleName))}.");
        SendPanelKeyForSmoke(0x28); // VK_DOWN
        NativeSmokeHarness.Require(panel.FocusedTarget == items[0].Key, "Down must move from the More button into the action list.");
        if (_installedApplications.Length > 0)
        {
            // "Open in" shows each installed application under it, drawn with its icon or the glyph.
            SendPanelKeyForSmoke(0x0D); // VK_RETURN
            NativeSmokeHarness.Require(panel.Scene.Targets.Where(target => target.Kind == PanelTargetKind.OpenIn).Select(target => target.AccessibleName)
                    .SequenceEqual(_installedApplications.Select(application => $"Open {name} in {application.Title}"))
                && panel.Scene.Commands.OfType<AppIconCommand>().Count() == _installedApplications.Length,
                "Open in must list every installed application with an icon.");
            // Paint now, so a failing icon extraction or DrawIconEx surfaces in smoke.
            NativeMethods.UpdateWindow(_panelWindow);
            SendPanelKeyForSmoke(0x1B); // VK_ESCAPE
            NativeSmokeHarness.Require(panel.FocusedTarget == items[0].Key && !panel.Scene.Targets.Any(target => target.Kind == PanelTargetKind.OpenIn),
                "Escape must hide the Open in applications and return to Open in.");
        }
        SendPanelKeyForSmoke(0x1B); // VK_ESCAPE
        NativeSmokeHarness.Require(panel.ActionsRow is null && _panelVisible && panel.FocusedTarget?.Kind == PanelTargetKind.More,
            "Escape must close the action list, keep the panel open and return focus to More.");
    }
 
    /// <summary>Runs an action as if the panel had produced it from a scene that is now out of date.</summary>
    internal void RunPanelActionForSmoke(PanelAction action)
    {
        EnsurePanelForSmoke();
        RunPanelAction(action);
    }
 
    /// <summary>Hides and reopens the panel, which revalidates pinned AppHosts like a user reopening it.</summary>
    internal void ReopenPanelForSmoke()
    {
        HidePanel();
        _panelHiddenAt = 0;
        ShowPanel();
    }
 
    /// <summary>
    /// Checks that every notification the icon sends opens the panel: a left click (NIN_SELECT), a
    /// right click or the context-menu key (WM_CONTEXTMENU) and keyboard selection (NIN_KEYSELECT).
    /// Keyboard activations also show the keyboard focus; for WM_CONTEXTMENU that is decided from
    /// the pointer position, so a synthetic one counts as keyboard unless the pointer happens to
    /// rest on the icon. Also checks Ctrl+comma and the Settings view with isolated startup settings.
    /// </summary>
    internal void VerifyActivationForSmoke()
    {
        RequireSmoke();
        foreach (var (notification, expected) in new (uint, bool?)[]
        {
            (NativeMethods.NinSelect, false), (NativeMethods.WmContextMenu, null), (NativeMethods.NinKeySelect, true)
        })
        {
            HidePanel();
            _panelHiddenAt = 0;
            var keyboard = expected ?? IsKeyboardContextMenu();
            NativeMethods.SendMessage(_window, NativeMethods.TrayCallback, 0, (nint)notification);
            NativeSmokeHarness.Require(_panelVisible && (_panel!.FocusedTarget is not null) == keyboard,
                $"Notification 0x{notification:x} must open the panel{(keyboard ? " with keyboard focus" : "")}.");
            // A repeated notification right after opening (a double-click) keeps it open.
            NativeMethods.SendMessage(_window, NativeMethods.TrayCallback, 0, (nint)notification);
            NativeSmokeHarness.Require(_panelVisible, "A repeated activation closed the panel it had just opened.");
            _panelShownAt = 0;
            NativeMethods.SendMessage(_window, NativeMethods.TrayCallback, 0, (nint)notification);
            NativeSmokeHarness.Require(!_panelVisible, "Activating the icon again must close the panel.");
        }
        _panelHiddenAt = 0;
        ShowPanel();
        NativeSmokeHarness.Require(!_panel!.IsSettingsOpen, "Settings opened unexpectedly.");
        // Ctrl+comma: mark Ctrl down in this thread's key state table, which GetKeyState reads,
        // rather than synthesizing global input with SendInput.
        // https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setkeyboardstate
        var keys = new byte[256];
        fixed (byte* state = keys)
        {
            NativeCallException.Require(NativeMethods.GetKeyboardState(state) != 0, "GetKeyboardState(smoke)");
            var control = keys[0x11];
            keys[0x11] = 0x80;
            NativeCallException.Require(NativeMethods.SetKeyboardState(state) != 0, "SetKeyboardState(smoke)");
            try
            {
                SendPanelKeyForSmoke(0xBC); // VK_OEM_COMMA
            }
            finally
            {
                keys[0x11] = control;
                NativeMethods.SetKeyboardState(state);
            }
        }
        NativeSmokeHarness.Require(_panel.IsSettingsOpen, "Ctrl+comma must open the Settings view in the panel.");
        SendPanelKeyForSmoke(0x1B); // VK_ESCAPE
        NativeSmokeHarness.Require(!_panel.IsSettingsOpen && _panelVisible, "Escape must leave the Settings view and keep the panel open.");
        HidePanel();
    }
 
    /// <summary>
    /// Opens the Settings view with the gear and checks its launch-at-sign-in switch against the
    /// isolated startup settings: <paramref name="enabled"/> is whether it can be pressed and
    /// <paramref name="description"/> the text under it.
    /// </summary>
    internal void VerifySettingsViewForSmoke(bool on, bool enabled, string description)
    {
        var panel = EnsurePanelForSmoke();
        if (!panel.IsSettingsOpen)
        {
            ClickPanelForSmoke(PanelTargetKind.Settings);
        }
        else
        {
            // Re-read the settings, as reopening the view does.
            RunPanelAction(new(PanelActionKind.Settings));
        }
        var toggle = panel.Scene.Accessibility.Single(element => element.Target?.Kind == PanelTargetKind.LaunchAtSignIn);
        NativeSmokeHarness.Require(toggle.Role == PanelAccessibleRole.Switch && toggle.Checked == on && toggle.Enabled == enabled
            && toggle.Description == description,
            $"The Settings switch misrepresents startup state: checked={toggle.Checked}, enabled={toggle.Enabled}, description={toggle.Description}.");
    }
 
    internal void ToggleLaunchAtSignInForSmoke()
    {
        EnsurePanelForSmoke();
        ClickPanelForSmoke(PanelTargetKind.LaunchAtSignIn);
    }
 
    internal void CloseSettingsViewForSmoke()
    {
        if (_panel is { IsSettingsOpen: true })
        {
            ClickPanelForSmoke(PanelTargetKind.Back);
        }
    }
 
    internal void RestartExplorerForSmoke()
    {
        RequireSmoke();
        NativeCallException.Require(NativeMethods.ShellNotifyIcon(NativeMethods.NimDelete, ref _iconData) != 0, "Shell_NotifyIconW(smoke delete)");
        _iconAdded = false;
        NativeMethods.SendMessage(_window, _taskbarCreated, 0, 0);
        NativeSmokeHarness.Require(_iconAdded, "Explorer restart did not restore the native icon.");
    }
 
    internal void InvalidateArtworkForSmoke()
    {
        RequireSmoke();
        // A synthetic notification exercises invalidation, not a real monitor-DPI transition.
        NativeMethods.SendMessage(_window, NativeMethods.WmDpiChanged, 0, 0);
    }
 
    internal void VerifyNativeStateForSmoke()
    {
        RequireSmoke();
        var state = controller.State;
        NativeSmokeHarness.Require(_artwork is { Original: not 0, Running: not 0, Attention: not 0, Dimmed: not 0 },
            "Native icon ownership is incomplete.");
        NativeSmokeHarness.Require(_iconAdded && _iconData.Icon == _artwork!.TrayIcon(TrayIconStatusRules.Get(state)),
            "The native tray icon does not represent discovery and active AppHosts.");
        // Connecting and unavailable deliberately share the dimmed logo; the tooltip tells them apart.
        NativeSmokeHarness.Require(Enum.GetValues<TrayIconStatus>().Select(_artwork!.TrayIcon).Distinct().Count() == 4
            && _artwork.TrayIcon(TrayIconStatus.Connecting) == _artwork.TrayIcon(TrayIconStatus.Unavailable),
            "Idle, running, attention, and dimmed need distinct notification icons.");
        var panel = EnsurePanelForSmoke();
        var rows = panel.Scene.Targets.Where(target => target.Kind == PanelTargetKind.Row && target.Key.Section != PanelSection.Recent).ToList();
        NativeSmokeHarness.Require(rows.Select(row => row.Host!.Id).SequenceEqual(
                state.AppHosts.Where(host => host.Id.AppHostPid != 0).Concat(state.AppHosts.Where(host => host.Id.AppHostPid == 0)).Select(host => host.Id)),
            "The panel does not list the AppHosts of the shared view state in order.");
        foreach (var row in rows)
        {
            var host = state.AppHosts.Single(host => host.Id == row.Host!.Id);
            bool Enabled(PanelTargetKind kind) => panel.Scene.Targets.FirstOrDefault(target => target.Key == row.Key with { Kind = kind })?.Enabled == true;
            NativeSmokeHarness.Require(row.AccessibleName.StartsWith($"{host.DisplayName}, ", StringComparison.Ordinal)
                && Enabled(PanelTargetKind.Dashboard) == host.CanOpenDashboard && Enabled(PanelTargetKind.Stop) == host.CanStop
                && Enabled(PanelTargetKind.Start) == host.CanStart, "A panel row misrepresents its AppHost's actions.");
        }
        NativeSmokeHarness.Require(_iconData.CallbackMessage == NativeMethods.TrayCallback, "The icon must report activations to the panel.");
        NativeSmokeHarness.Require(panel.Scene.Accessibility.Single(element => element.Key.Label == "header.status").Bounds.CenterY == TrayPanel.HeaderHeight / 2
            && TrayPanel.HeaderHeight == TrayPanel.FooterHeight, "The header must be one line, as tall as the footer.");
        NativeSmokeHarness.Require(_panelTooltip != 0 && _panelTooltips.Select(tooltip => tooltip.Text).SequenceEqual(
                panel.Scene.Targets.Where(target => target.Tooltip is not null && !target.HitBounds.IsEmpty).Select(target => target.Tooltip!))
            && NativeMethods.SendMessage(_panelTooltip, 0x040D, 0, 0) == _panelTooltips.Count, // TTM_GETTOOLCOUNT
            "Every visible icon button must have exactly one native tooltip.");
        NativeSmokeHarness.Require(panel.Scene.Accessibility.Single(element => element.Target?.Kind == PanelTargetKind.Quit) is
            { Name: "Exit", Description: "Exit the tray. AppHosts keep running.", Target.Tooltip: "Exit the tray. AppHosts keep running." },
            "The Exit button must say it closes only the tray.");
    }
 
    private void RequireSmoke()
    {
        if (smokeSeconds is null)
        {
            throw new InvalidOperationException("Native smoke hooks are unavailable in normal mode.");
        }
    }
 
    private sealed record SmokeDialog(int DefaultId, int Response);
}