XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEWorkflow

【WinUI】XFE基于windows的工作流

公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Automation.Provider;
using Microsoft.UI.Xaml.Media;
using XFEWorkflow.Abstractions;
using XFEWorkflow.Controls;
using XFEWorkflow.Core.Runtime;
using XFEWorkflow.Infrastructure.Storage;
using XFEWorkflow.ViewModels;
using XFEWorkflow.Views;

namespace XFEWorkflow.Services;

internal static partial class UiSmokeTest
{
    internal static IEnumerable<T> Descendants<T>(DependencyObject root) where T : DependencyObject
    {
        if (root is T target) yield return target;
        for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++) foreach (var item in Descendants<T>(VisualTreeHelper.GetChild(root, i))) yield return item;
    }
    internal static async Task InvokeButtonAsync(Button button)
    {
        if (!button.IsEnabled) throw new InvalidOperationException("按钮不可用:" + button.Content);
        var peer = FrameworkElementAutomationPeer.FromElement(button) ?? new ButtonAutomationPeer(button);
        ((IInvokeProvider)peer.GetPattern(PatternInterface.Invoke)).Invoke(); await Task.Delay(180);
    }
    private static async Task WaitManagementAsync(Func<bool> ready)
    { for (var i = 0; i < 80; i++) { if (ready()) return; await Task.Delay(50); } throw new InvalidOperationException("页面没有加载期望的数据"); }
    private static async Task SmokeManagementPagesAsync(string output)
    {
        var services = AppServices.Current; var shell = AppShellPage.Current!; var navigation = shell.ViewModel.NavigationViewService;
        var frame = (Frame)shell.FindName("navigationFrame"); var root = (FrameworkElement)App.MainWindow.Content; var theme = root.RequestedTheme; var size = App.MainWindow.AppWindow.Size;
        var scale = root.XamlRoot.RasterizationScale;
        void Resize(int width, int height) => App.MainWindow.AppWindow.Resize(new((int)(width * scale), (int)(height * scale)));
        var workflow = WorkflowTemplates.CreateGraph(); workflow.Name = "页面联动验收";
        const string logMessage = "工作流运行日志验收\n第二行:输入与输出完整保留";
        var logNode = new NodeDefinition { CapabilityId = "core.log", Name = "界面日志", Parameters = new() { ["message"] = logMessage } };
        workflow.Graph.Nodes.Insert(1, logNode); workflow.Graph.Edges.Clear();
        for (var i = 0; i < workflow.Graph.Nodes.Count - 1; i++) workflow.Graph.Edges.Add(new() { FromNode = workflow.Graph.Nodes[i].Id, ToNode = workflow.Graph.Nodes[i + 1].Id });
        var runInputs = new System.Text.Json.Nodes.JsonObject { ["message"] = "第一行\n第二行", ["options"] = new System.Text.Json.Nodes.JsonObject { ["count"] = 2 } };
        await services.Workflows.SaveAsync(workflow); var run = await services.Runs.RunAsync(workflow, inputs: runInputs);
        if (run.Status != RunStatus.Succeeded) throw new InvalidOperationException(run.Error);
        try
        {
            shell.ShowBrowser(false); shell.SetNavigationOpenForSmoke(false); Resize(1440, 960); root.RequestedTheme = ElementTheme.Dark;
            navigation.NavigateTo<PluginsPage>(); await WaitManagementAsync(() => frame.Content is PluginsPage && Descendants<GridView>(frame).Any(x => x.Items.Count > 0));
            var plugins = (PluginsPage)frame.Content;
            if (Descendants<Pivot>(plugins).Any() || !Descendants<Button>(plugins).Any(x => AutomationProperties.GetAutomationId(x) == "OpenSpace")) throw new InvalidOperationException("插件中心没有将 Space 独立为页面");
            var catalog = Descendants<GridView>(plugins).Single(); if (catalog.Items.Any(x => x is not PluginCardModel)) throw new InvalidOperationException("插件卡片没有真实清单数据");
            var pluginPanels = Descendants<DetailsWorkspace>(plugins).Single(); await Task.Delay(300); var previousWidth = pluginPanels.Details.ActualWidth;
            await CaptureWindowAsync(Path.Combine(output, "plugins-before-resize.png"));
            App.MainWindow.Activate(); SetForegroundWindow(WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow)); await Task.Delay(150);
            await DragElementAsync(pluginPanels.Splitter, -30, 0);
            await AtomicJson.WriteAsync(Path.Combine(output, "plugins-splitter.json"), new { before = previousWidth, after = pluginPanels.Details.ActualWidth, width = pluginPanels.ActualWidth, height = pluginPanels.ActualHeight, orientation = pluginPanels.Splitter.Orientation.ToString(), preferred = pluginPanels.Splitter.PreferredSize });
            if (Math.Abs(pluginPanels.Details.ActualWidth - previousWidth - 30) > 4) throw new InvalidOperationException($"插件详情面板无法拖动调整:{previousWidth} → {pluginPanels.Details.ActualWidth}, orientation={pluginPanels.Splitter.Orientation}");
            await CaptureWindowAsync(Path.Combine(output, "plugins-center-dark.png"));
            root.RequestedTheme = ElementTheme.Light; await Task.Delay(100); await CaptureWindowAsync(Path.Combine(output, "plugins-center-light.png"));
            root.RequestedTheme = ElementTheme.Dark;
            navigation.NavigateTo<RunsPage>(run.Id); await WaitManagementAsync(() => frame.Content is RunsPage && Descendants<ListView>(frame).Any(x => x.SelectedItem is RunRowModel row && row.Record.Id == run.Id));
            var runInputBox = (TextBox)Descendants<Expander>(frame).Single(x => x.Header as string == "输入参数").Content;
            if (!System.Text.Json.Nodes.JsonNode.DeepEquals(System.Text.Json.Nodes.JsonNode.Parse(runInputBox.Text), runInputs)) throw new InvalidOperationException("运行记录的多行 JSON 输入显示不完整");
            await CaptureWindowAsync(Path.Combine(output, "runs-master-details.png"));
            await InvokeButtonAsync(Descendants<Button>(frame).Single(x => AutomationProperties.GetAutomationId(x) == "RunFullLog"));
            await WaitManagementAsync(() => frame.Content is LogViewPage && Descendants<ListView>(frame).Any(x => x.Items.OfType<WorkflowLogEntry>().Any(e => e.RunId == run.Id && e.LogText.Contains("工作流运行日志验收"))));
            var logs = Descendants<ListView>(frame).First(x => x.Items.OfType<WorkflowLogEntry>().Any());
            if (logs.Items.OfType<WorkflowLogEntry>().Any(x => x.RunId != run.Id)) throw new InvalidOperationException("运行记录到日志的筛选未生效");
            logs.SelectedItem = logs.Items.OfType<WorkflowLogEntry>().First(x => x.NodeId == logNode.Id && x.LogText.Contains(logMessage)); await Task.Delay(150);
            var logDetailBoxes = Descendants<TextBox>(frame).Where(x => x.IsReadOnly).ToArray();
            await AtomicJson.WriteAsync(Path.Combine(output, "logs-multiline-details.json"), new { expected = logMessage, actual = logDetailBoxes.Select(x => x.Text) });
            if (!logDetailBoxes.Any(x => x.Text.ReplaceLineEndings("\n").Contains(logMessage))) throw new InvalidOperationException("日志详情的多行消息被截断");
            await CaptureWindowAsync(Path.Combine(output, "logs-workflow-run-filter.png"));
            await InvokeButtonAsync(Descendants<Button>(frame).Single(x => AutomationProperties.GetAutomationId(x) == "LocateLogWorkflow"));
            await WaitManagementAsync(() => frame.Content is WorkflowEditorPage && WorkflowEditorPage.Current?.ActiveEditor?.ViewModel.SelectedWorkflow?.Id == workflow.Id);
            navigation.NavigateTo<SettingPage>("credentials"); await WaitManagementAsync(() => frame.Content is SettingPage && Descendants<Pivot>(frame).FirstOrDefault()?.SelectedIndex == 1);
            if (!Descendants<CredentialSettings>(frame).Any()) throw new InvalidOperationException("凭据没有移入设置页面");
            await CaptureWindowAsync(Path.Combine(output, "settings-credentials.png"));
            var settings = (SettingPage)frame.Content; var settingsTabs = Descendants<Pivot>(settings).First(); settingsTabs.SelectedIndex = 2; await Task.Delay(500);
            if (!Descendants<DesktopInputSettings>(settings).Any()) throw new InvalidOperationException("桌面输入没有移入设置页面");
            var driverStatus = Descendants<TextBlock>(settings).Single(x => AutomationProperties.GetAutomationId(x) == "DriverStatus").Text;
            if (string.IsNullOrWhiteSpace(driverStatus) || driverStatus.Contains(nameof(XFEExtension.NetCore.InputSimulator.DriverCheckResult))) throw new InvalidOperationException("HID 状态显示了类型名而非检测说明");
            await CaptureWindowAsync(Path.Combine(output, "settings-desktop-input.png"));
            settingsTabs.SelectedIndex = 0; await Task.Delay(500);
            if (!Descendants<TextBlock>(settings).Any(x => x.Text == ClientState.LocalServerAddress) || Descendants<TextBox>(settings).Any(x => x.Text == ClientState.LocalServerAddress)) throw new InvalidOperationException("服务器地址未固定或仍可修改");
            foreach (var page in new[] { typeof(PluginsPage), typeof(SpacePage), typeof(RunsPage), typeof(LogViewPage), typeof(SettingPage) })
            {
                Resize(760, 940); navigation.NavigateTo(page.FullName!); await Task.Delay(280);
                var panel = Descendants<DetailsWorkspace>(frame).FirstOrDefault();
                if (panel is not null && (panel.Splitter.Orientation != Orientation.Horizontal || panel.Master.ActualHeight < 100 || panel.Details.ActualHeight < 100)) throw new InvalidOperationException("窄窗口详情布局不可用:" + page.Name);
                await CaptureWindowAsync(Path.Combine(output, page.Name + "-narrow.png"));
            }
        }
        finally { root.RequestedTheme = theme; App.MainWindow.AppWindow.Resize(size); navigation.NavigateTo<MainPage>(); }
    }
}