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

XFEWorkflow

【WinUI】XFE基于windows的工作流

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

namespace XFEWorkflow.Services;

internal static partial class UiSmokeTest
{
    private static async Task SmokeWorkflowDocumentsAsync(string output)
    {
        var services = AppServices.Current; var navigation = AppShellPage.Current!.ViewModel.NavigationViewService;
        var root = (FrameworkElement)App.MainWindow.Content; var originalSize = App.MainWindow.AppWindow.Size; var originalTheme = root.RequestedTheme;
        var paletteSize = services.State.PaneSize("workflow.palette", 260);
        var outputSize = services.State.PaneSize("workflow.output", 156);
        var scale = root.XamlRoot.RasterizationScale; App.MainWindow.AppWindow.Resize(new((int)(1600 * scale), (int)(1000 * scale)));
        var first = WorkflowTemplates.Welcome(); first.Name = "标签页 A"; first.Enabled = false; first.Triggers.Add(new() { Kind = TriggerKind.Interval, Enabled = true, IntervalSeconds = 3600 });
        var second = WorkflowTemplates.Welcome(); second.Name = "标签页 B"; second.Enabled = false;
        await services.Workflows.SaveAsync(first); await services.Workflows.SaveAsync(second);
        try
        {
            navigation.NavigateTo<WorkflowListPage>(); await WaitManagementAsync(() => Descendants<Frame>(App.MainWindow.Content).Any(x => x.Content is WorkflowListPage));
            await WaitManagementAsync(() => Descendants<Button>(App.MainWindow.Content).Any(x => AutomationProperties.GetAutomationId(x) == "EditWorkflow-" + first.Id));
            var firstRow = Descendants<Expander>(App.MainWindow.Content).Single(x => x.Tag as string == first.Id); firstRow.IsExpanded = false;
            await Task.Delay(300); await CaptureWindowAsync(Path.Combine(output, "workflow-list.png"));
            await InvokeButtonAsync(Descendants<Button>(firstRow).Single(x => AutomationProperties.GetAutomationId(x) == "EditWorkflow-" + first.Id));
            await WaitManagementAsync(() => WorkflowEditorPage.Current?.ActiveEditor?.ViewModel.SelectedWorkflow?.Id == first.Id);
            if (firstRow.IsExpanded) throw new InvalidOperationException("列表右侧编辑按钮不应展开工作流卡片");
            var editorNavigation = Descendants<NavigationViewItem>(AppShellPage.Current!).Single(x => x.Content as string == "工作流编辑器");
            if (editorNavigation.Icon is not FontIcon { Glyph: "\uE943" }) throw new InvalidOperationException("工作流编辑器入口没有使用代码图标");
            var hub = WorkflowEditorPage.Current!; var a = hub.ActiveEditor!; a.ViewModel.SelectedWorkflow!.Description = "A 独立修改";
            var b = await hub.OpenAsync(second.Id); b.ViewModel.SelectedWorkflow!.Description = "B 独立修改";
            await hub.OpenAsync(first.Id);
            if (!ReferenceEquals(hub.ActiveEditor, a) || hub.Documents.TabItems.Count != 2 || a.ViewModel.SelectedWorkflow!.Description != "A 独立修改") throw new InvalidOperationException("多标签页未保留独立编辑状态或重复打开");
            if (Descendants<CommandBar>(a).Any(x => x.Visibility == Visibility.Visible) || Descendants<Expander>(a).Any(x => x.Name == "WorkflowLibraryExpander" && x.Visibility == Visibility.Visible)) throw new InvalidOperationException("编辑器仍展示工作流列表和旧溢出工具栏");
            if (!Descendants<MenuBar>(hub).Single().Items.Select(x => x.Title).SequenceEqual(new[] { "文件", "编辑", "工作流", "视图", "工具" })) throw new InvalidOperationException("顶部菜单不完整");
            if (!a.ViewModel.PaletteGroups.Any(x => x.Name == "鼠标操作" && x.Glyph == "\uE962") || !a.ViewModel.PaletteGroups.Any(x => x.Name == "键盘操作" && x.Glyph == "\uE765")) throw new InvalidOperationException("鼠标或键盘分类图标错误");
            var dockingEvidence = new List<object>(); var colors = new Dictionary<ElementTheme, Windows.UI.Color>();
            async Task VerifyDockingAsync(string stage)
            {
                await Task.Delay(200);
                var palette = Descendants<Border>(hub).Single(x => x.Name == "PalettePanel");
                var header = Descendants<Grid>(hub.ActiveEditor!).Single(x => x.Name == "EditorModeHeader");
                var selected = (TabViewItem)hub.Documents.SelectedItem;
                var stripOrigin = hub.Documents.TransformToVisual(hub).TransformPoint(new(0, 0));
                var headerOrigin = header.TransformToVisual(hub).TransformPoint(new(0, 0));
                var tabBottom = selected.TransformToVisual(hub).TransformPoint(new(0, selected.ActualHeight)).Y;
                var paletteOrigin = palette.TransformToVisual(hub).TransformPoint(new(0, 0));
                if (Math.Abs(stripOrigin.X - headerOrigin.X) > 1 || Math.Abs(tabBottom - headerOrigin.Y) > 1 || stripOrigin.X - paletteOrigin.X < 200 || Math.Abs(paletteOrigin.Y - stripOrigin.Y) > 2)
                    throw new InvalidOperationException($"工作流标签与编辑区没有对齐或存在间隙:{stage} · strip={stripOrigin} · header={headerOrigin} · tabBottom={tabBottom}");
                var outputPanel = Descendants<Border>(hub).Single(x => x.Name == "OutputPanel");
                var outputOrigin = outputPanel.TransformToVisual(hub).TransformPoint(new(0, 0));
                var documentBottom = hub.DocumentSurface.TransformToVisual(hub).TransformPoint(new(0, hub.DocumentSurface.ActualHeight)).Y;
                if (Descendants<Border>(hub.DocumentSurface).Contains(outputPanel) || Math.Abs(outputOrigin.X - hub.DocumentSurface.TransformToVisual(hub).TransformPoint(new(0, 0)).X) > 1 || Math.Abs(outputOrigin.Y - documentBottom - 8) > 1)
                    throw new InvalidOperationException("运行输出没有与上方编辑窗口分离:" + stage);
                if (VisualTreeHelper.GetParent(outputPanel) != VisualTreeHelper.GetParent(hub.DocumentSurface) || VisualTreeHelper.GetParent(palette) != VisualTreeHelper.GetParent(outputPanel))
                    throw new InvalidOperationException("输出、编辑器和节点库应为相互独立的面板");
                var selectedPath = Descendants<Microsoft.UI.Xaml.Shapes.Path>(selected).Single(x => x.Name == "SelectedBackgroundPath");
                var color = ((SolidColorBrush)header.Background).Color;
                if (selectedPath.Fill is not SolidColorBrush fill || fill.Color != color)
                    throw new InvalidOperationException("工作流选项卡与内部编辑窗口未形成统一主题背景:" + stage);
                if (hub.DocumentSurface.Background is not SolidColorBrush surface || palette.Background is not SolidColorBrush paletteBrush || surface.Color != paletteBrush.Color || surface.Color == color)
                    throw new InvalidOperationException("编辑器外层应使用与节点库相同的控件背景:" + stage);
                var status = Descendants<Border>(hub).Single(x => x.Name == "EditorStatus");
                if (status.BorderThickness.Left < 1 || status.CornerRadius.TopLeft < 4 || status.Padding.Top < 4)
                    throw new InvalidOperationException("底部状态输出未包裹在控件中:" + stage);
                colors[root.RequestedTheme] = color;
                dockingEvidence.Add(new { stage, theme = root.RequestedTheme.ToString(), stripLeft = stripOrigin.X, headerLeft = headerOrigin.X, tabBottom, headerTop = headerOrigin.Y, paletteWidth = palette.ActualWidth, documentBottom, outputTop = outputOrigin.Y, outputHeight = outputPanel.ActualHeight, background = color.ToString() });
            }
            root.RequestedTheme = ElementTheme.Dark; await VerifyDockingAsync("dark");
            var initialViewport = Descendants<Controls.WorkflowCanvas>(a).Single().InspectViewport();
            var startX = initialViewport["startScreenX"]!.GetValue<double>();
            if (startX < 0 || startX > initialViewport["viewportWidth"]!.GetValue<double>()) throw new InvalidOperationException("首次打开编辑器未将开始节点置于可见范围:" + initialViewport.ToJsonString());
            var dockedSplitter = Descendants<Controls.PaneSplitter>(hub).Single(x => AutomationProperties.GetName(x) == "调整节点库宽度");
            var oldPaletteWidth = Descendants<Border>(hub).Single(x => x.Name == "PalettePanel").ActualWidth;
            services.State.SetPaneSize("workflow.palette", oldPaletteWidth + 48); dockedSplitter.ReloadPreference(); await VerifyDockingAsync("resized");
            if (Math.Abs(Descendants<Border>(hub).Single(x => x.Name == "PalettePanel").ActualWidth - oldPaletteWidth - 48) > 4) throw new InvalidOperationException("调整节点库宽度后标签与编辑器未随动");
            await hub.OpenAsync(second.Id); await VerifyDockingAsync("second-tab");
            await hub.OpenAsync(first.Id); await VerifyDockingAsync("first-tab-restored");
            var outputTabs = Descendants<TabView>(hub).Single(x => x.Name == "RunPanel"); outputTabs.SelectedIndex = 1;
            var outputSplitter = Descendants<Controls.PaneSplitter>(hub).Single(x => AutomationProperties.GetName(x) == "调整运行输出高度");
            services.State.SetPaneSize("workflow.output", 220); outputSplitter.ReloadPreference(); await VerifyDockingAsync("output-resized");
            if (Math.Abs(Descendants<Border>(hub).Single(x => x.Name == "OutputPanel").ActualHeight - 220) > 1) throw new InvalidOperationException("独立输出面板不能调整高度");
            await a.ExecuteEditorCommandAsync("output"); await Task.Delay(100);
            if (Math.Abs(Descendants<Border>(hub).Single(x => x.Name == "OutputPanel").ActualHeight - 42) > 1) throw new InvalidOperationException("独立输出面板未收起");
            await hub.OpenAsync(second.Id); await VerifyDockingAsync("output-other-tab");
            if (Descendants<TabView>(hub).Single(x => x.Name == "RunPanel").SelectedIndex != 0) throw new InvalidOperationException("工作流输出标签状态相互覆盖");
            await hub.OpenAsync(first.Id); await Task.Delay(100);
            if (!ReferenceEquals(outputTabs, Descendants<TabView>(hub).Single(x => x.Name == "RunPanel")) || outputTabs.SelectedIndex != 1 || Math.Abs(Descendants<Border>(hub).Single(x => x.Name == "OutputPanel").ActualHeight - 42) > 1)
                throw new InvalidOperationException("切回工作流未恢复输出标签和收起状态");
            await a.ExecuteEditorCommandAsync("output"); outputTabs.SelectedIndex = 0; await VerifyDockingAsync("output-restored");
            await CaptureWindowAsync(Path.Combine(output, "workflow-editor-connected-tabs-dark.png"));
            root.RequestedTheme = ElementTheme.Light; await VerifyDockingAsync("light");
            await CaptureWindowAsync(Path.Combine(output, "workflow-editor-connected-tabs-light.png"));
            if (colors[ElementTheme.Dark] == colors[ElementTheme.Light]) throw new InvalidOperationException("工作流编辑区没有适配深浅主题");
            await Infrastructure.Storage.AtomicJson.WriteAsync(Path.Combine(output, "workflow-document-docking.json"), dockingEvidence);
            root.RequestedTheme = originalTheme;
            var search = Descendants<TextBox>(hub).Single(x => x.Name == "PaletteSearch"); search.Text = "输出日志";
            await WaitManagementAsync(() => a.ViewModel.PaletteRows.OfType<PaletteItem>().Any(x => x.Descriptor.Id == "core.log"));
            var paletteList = Descendants<ListView>(hub).Single(x => x.Name == "PaletteList"); var logItem = a.ViewModel.PaletteRows.OfType<PaletteItem>().Single(x => x.Descriptor.Id == "core.log");
            paletteList.ScrollIntoView(logItem); await WaitManagementAsync(() => paletteList.ContainerFromItem(logItem) is FrameworkElement);
            var nodeCount = a.ViewModel.SelectedWorkflow!.Graph.Nodes.Count;
            var container = (ListViewItem)paletteList.ContainerFromItem(logItem);
            var peer = FrameworkElementAutomationPeer.FromElement(container) ?? new ListViewItemAutomationPeer(container);
            if (peer.GetPattern(PatternInterface.Invoke) is not IInvokeProvider invoke) throw new InvalidOperationException("节点库项缺少自动化调用入口");
            invoke.Invoke();
            await WaitManagementAsync(() => a.ViewModel.SelectedWorkflow!.Graph.Nodes.Count == nodeCount + 1);
            await a.ExecuteEditorCommandAsync("undo"); if (a.ViewModel.SelectedWorkflow!.Graph.Nodes.Count != nodeCount) throw new InvalidOperationException("独立节点库添加与撤销未作用到当前工作流"); search.Text = "";
            await services.Workflows.SetActivationAsync(first.Id, true, 9); await hub.SaveAllAsync();
            if ((await services.Workflows.GetAsync(first.Id)).Enabled != true) throw new InvalidOperationException("编辑器保存覆盖了列表的启停状态");
            await CaptureWindowAsync(Path.Combine(output, "workflow-editor-multiple-tabs-menus.png"));
            navigation.NavigateTo<WorkflowListPage>(); await Task.Delay(300);
            var frame = Descendants<Frame>(App.MainWindow.Content).Single(x => x.Content is WorkflowListPage); var list = (WorkflowListPage)frame.Content;
            await list.SetEnabledAsync(first.Id, false);
            if (services.Scheduler.StatusFor(await services.Workflows.GetAsync(first.Id)).Any(x => x.Enabled)) throw new InvalidOperationException("列表关闭后仍在调度");
            navigation.NavigateTo<WorkflowEditorPage>(); await Task.Delay(150); if (hub.Documents.TabItems.Count != 2) throw new InvalidOperationException("离开编辑器后标签页丢失");
        }
        finally
        {
            await WorkflowEditorPage.CloseOpenAsync(first.Id); await WorkflowEditorPage.CloseOpenAsync(second.Id); navigation.NavigateTo<MainPage>();
            services.State.SetPaneSize("workflow.palette", paletteSize); services.State.SetPaneSize("workflow.output", outputSize); await services.State.SaveAsync(); root.RequestedTheme = originalTheme; App.MainWindow.AppWindow.Resize(originalSize);
        }
    }
}