using System.Text.Json; using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Media; using XFEEDToolBox.Views; using XFEEDToolBox.Core.Galaxy; using XFEEDToolBox.Core.Models; namespace XFEEDToolBox; public partial class App { private sealed record TradeUiCheck(string Item,string Status,string Evidence); private static async Task RunTradePlannerProbeAsync(string directory) { var checks=new List();var captures=new List(); object? defaultLayout=null,plannedLayout=null,editLayout=null,editBottomLayout=null,executionLayout=null; object? initialInvestment=null; string? calculationSkipped=null,failure=null; object? regionFixture=null; object? planningVisualFixture=null; var isolatedDirectory=Path.GetFullPath(Path.Combine(directory,"isolated-data")); var isolatedFixture=ViewModel.OfflinePreview&&Path.GetFullPath(Runtime.DataDirectory).Equals(isolatedDirectory,StringComparison.OrdinalIgnoreCase); void Check(string name,bool? passed,string evidence)=>checks.Add(new(name,passed is null?"unconfirmed":passed.Value?"passed":"failed",evidence)); if(ViewModel.DemoMode)throw new InvalidOperationException("Trade UI acceptance must use actual local player and market data."); AppShellPage.Current!.Navigate(typeof(PlannerPage));await Task.Delay(250); var page=(PlannerPage)AppShellPage.Current.CurrentPage!; var scroller=(ScrollViewer)page.Content; FrameworkElement Find(string id)=>page.FindName(id) as FrameworkElement ??Descendants(page).OfType().FirstOrDefault(element=>AutomationProperties.GetAutomationId(element)==id) ??throw new InvalidOperationException("The trade page does not contain the named element: "+id); var advancedSearch=(Expander)Find("TradeAdvancedSearchExpander"); var advancedPeer=new ExpanderAutomationPeer(advancedSearch); bool Visible(FrameworkElement element) { DependencyObject? current=element; while(current is not null) { if(current is UIElement ui&&ui.Visibility!=Visibility.Visible)return false; if(ReferenceEquals(current,page))break; current=VisualTreeHelper.GetParent(current); } return element.ActualWidth>0&&element.ActualHeight>0; } object Bounds(FrameworkElement element) { var visible=Visible(element); var origin=visible?element.TransformToVisual(page).TransformPoint(new(0,0)):(Windows.Foundation.Point?)null; return new{element.Name,x=origin?.X,y=origin?.Y,width=element.ActualWidth,height=element.ActualHeight, visibility=element.Visibility.ToString(),effectiveVisible=visible,right=origin?.X+element.ActualWidth,bottom=origin?.Y+element.ActualHeight}; } object Layout()=>new { pageWidth=page.ActualWidth,pageHeight=page.ActualHeight,viewportWidth=scroller.ViewportWidth, scrollableWidth=scroller.ScrollableWidth,verticalOffset=scroller.VerticalOffset,scrollableHeight=scroller.ScrollableHeight, investment=Bounds(Find("TradeInvestmentAmount")),planButton=Bounds(Find("CalculateTradeRoutesButton")), advancedExpander=Bounds(advancedSearch),advancedExpanded=advancedSearch.IsExpanded,advanced=Bounds(Find("TradeAdvancedOptions")), center=Bounds(Find("TradeRegionCenterInput")),radius=Bounds(Find("TradeRegionRadiusInput")),followCurrent=Bounds(Find("FollowCurrentTradeRegionButton")), execution=Bounds((FrameworkElement)page.FindName("TradeRunCard")),planningMode=Bounds(Find("TradePlanningModeInput")), ViewModel.TradePlanningModeIndex,ViewModel.TradeEditLabel, visibleNumberInputs=Descendants(page).OfType().Count(Visible), texts=Descendants(page).OfType().Where(Visible).Select(text=>text.Text).Where(text=>!string.IsNullOrWhiteSpace(text)).Distinct().ToArray() }; async Task Capture(string name) { page.UpdateLayout();await Task.Delay(100); var path=Path.Combine(directory,name+".png");await CapturePageAsync(page,path);captures.Add(path); } try { Check("advanced search initially collapsed",!advancedSearch.IsExpanded&&!ViewModel.TradeAdvancedEditing,"A newly opened planning page starts with the actual advanced-search Expander collapsed."); advancedPeer.Collapse();scroller.ChangeView(null,0,null,true);page.UpdateLayout();await Task.Delay(120); await Capture("trade-planner-default");defaultLayout=Layout(); var investment=Find("TradeInvestmentAmount");var calculate=Find("CalculateTradeRoutesButton"); var advanced=Find("TradeAdvancedOptions"); Check("default hides advanced controls",!advancedSearch.IsExpanded&&!ViewModel.TradeAdvancedEditing&&!Visible(advanced),"The real Expander is collapsed and its content is hidden, not merely off-screen."); var planningMode=(ComboBox)Find("TradePlanningModeInput"); Check("planning type is visible without advanced search",Visible(planningMode)&&planningMode.Items.Cast().SequenceEqual(new[]{"常规跑商","CG 供货"}),"The main bar exposes both planning types while advanced fields remain collapsed."); Check("advanced search replaces top-right button",advancedSearch.Header as string=="高级搜索选项"&&page.FindName("TradePlannerEditButton") is null,"The advanced-search header belongs to the inline Expander; the old top-right action is absent."); Check("advanced expansion respects execution ownership",advancedSearch.IsEnabled==ViewModel.TradeSelectionEnabled,"Expansion and its entire content share the existing idle-only selection gate."); if(isolatedFixture) { var savedMode=ViewModel.TradePlanningModeIndex; try { ViewModel.TradePlanningModeIndex=1;page.UpdateLayout();await Task.Delay(100); await Capture("trade-planner-cg-mode"); Check("main CG selection shares advanced mode",ViewModel.AutomaticModeIndex==1&&planningMode.SelectedIndex==1 &&((ComboBox)Find("AutomaticTradeMode")).SelectedIndex==1,"Both real XAML selectors bind the same mode; offline preview sends no native requests or game commands."); } finally{ViewModel.TradePlanningModeIndex=savedMode;page.UpdateLayout();} } Check("default shows investment and region radius",Descendants(page).OfType().Count(Visible)==2&&Visible(Find("TradeRegionRadiusInput"))&&Visible(Find("TradeRegionCenterInput"))&&Visible(Find("FollowCurrentTradeRegionButton")),"Investment, exact centre selection and radius remain visible; other constraints stay in Edit."); Check("saved shared radius is used",ViewModel.Radius==Runtime.Settings.MarketCacheRadiusLy&&double.IsFinite(ViewModel.Radius)&&ViewModel.Radius>0,$"Saved shared scope: {Runtime.Settings.MarketCacheCenterName}; radius {Runtime.Settings.MarketCacheRadiusLy} ly. New profiles default to 500 ly; explicit saved values are preserved."); Check("investment and planning controls visible",Visible(investment)&&Visible(calculate)&&Visible(advancedSearch),"Actual XAML layout includes investment, planning and the collapsed advanced-search header."); var advancedOrigin=advancedSearch.TransformToVisual(page).TransformPoint(new(0,0)); var regionInput=Find("TradeRegionRadiusInput");var regionOrigin=regionInput.TransformToVisual(page).TransformPoint(new(0,0)); Check("advanced search follows ordinary search",advancedOrigin.Y>=regionOrigin.Y+regionInput.ActualHeight,"The Expander is below the ordinary investment, mode and search-region controls."); var investmentOrigin=investment.TransformToVisual(page).TransformPoint(new(0,0));var calculateOrigin=calculate.TransformToVisual(page).TransformPoint(new(0,0)); Check("fullwidth investment bar fits",investmentOrigin.X>=0&&calculateOrigin.X+calculate.ActualWidth<=page.ActualWidth+1&&scroller.ScrollableWidth<1,"Investment and plan button fit the page without horizontal scrolling."); var player=ViewModel.CurrentPlayer; initialInvestment=new{ViewModel.TradeInvestment,ViewModel.TradeInvestmentMode,ViewModel.TradeFundsSummary,ViewModel.Reserve,player.Credits,player.Rebuy}; Check("investment defaults to all usable funds",ViewModel.TradeInvestmentMode=="全部可用资金"&&Math.Abs(ViewModel.TradeInvestment-Math.Max(0,(double)(player.Credits??0)-ViewModel.Reserve))<.01,"The initial investment follows the actual wallet after the operational and insurance reserve."); if(isolatedFixture) { var original=Runtime.Settings;var oldRadius=ViewModel.Radius; try { var center=new GalacticSystem("Isolated region fixture",new(120,3,-5),Source:"isolated-fixture"); ViewModel.RegisterTradeRegionSystemForProbe(center,isolatedDirectory); await ViewModel.CommitTradeRegionCenterAsync(center.Name); var saved=ToolboxSettings.Load(Runtime.SettingsPath); Check("isolated selected center is persisted",saved.MarketCacheCenterName==center.Name&&saved.MarketCacheCenterPosition==center.Position,"Explicit synthetic centre fixture only; no player, wallet, cargo or game state was substituted."); ViewModel.Radius=725;await ViewModel.SaveTradeRegionRadiusAsync();saved=ToolboxSettings.Load(Runtime.SettingsPath); Check("isolated radius commit is persisted",saved.MarketCacheRadiusLy==725&&saved.Planner.SearchRadiusLy==725,"The same committed radius controls cache admission and planning."); var bytes=await File.ReadAllBytesAsync(Runtime.SettingsPath); ViewModel.Radius=double.NaN; await ViewModel.SaveTradeRegionRadiusAsync(); var afterInvalid=await File.ReadAllBytesAsync(Runtime.SettingsPath); var emptyRadiusDefaulted=ViewModel.Radius==725&&bytes.SequenceEqual(afterInvalid); Check("empty radius retains saved default",emptyRadiusDefaulted,"Clearing the optional numeric field restores the last saved radius and leaves settings unchanged."); ViewModel.Radius=725;await ViewModel.FollowCurrentTradeRegionCommand.ExecuteAsync(null);saved=ToolboxSettings.Load(Runtime.SettingsPath); Check("restore-follow clears fixed center",saved.MarketCacheCenterName.Length==0&&saved.MarketCacheCenterPosition is null,"Follow-current stores no invented fixed coordinate."); regionFixture=new{explicitIsolatedFixture=true,center=center.Name,center.Position,savedRadius=725,emptyRadiusDefaulted,followRestored=true,gameActionsInvoked=false}; } finally { ViewModel.Radius=oldRadius; if(original.MarketCacheCenterPosition is {} originalPosition&&original.MarketCacheCenterName.Length>0) ViewModel.RegisterTradeRegionSystemForProbe(new(original.MarketCacheCenterName,originalPosition,Source:"isolated-fixture"),isolatedDirectory); await ViewModel.CommitTradeRegionCenterAsync(original.MarketCacheCenterName); await Runtime.SaveSettingsAsync(original); } } else Check("isolated center/radius persistence fixture",null,"Not exercised against the production profile; requires --offline-preview and report-directory/isolated-data."); var inputsReady=player.CreditsReliable&&player.CargoReliable&&player.Credits>player.Rebuy&&player.CargoCapacity>player.CargoCount&&player.MaxJumpRange is >0&&ViewModel.Stations.Count>0; if(inputsReady&&ViewModel.CalculateCommand.CanExecute(null)) { await ViewModel.CalculateCommand.ExecuteAsync(null);page.UpdateLayout();await Task.Delay(180); plannedLayout=Layout();await Capture("trade-planner-ranked-routes"); var plans=ViewModel.TradeCandidates.Select(candidate=>candidate.Plan).ToArray(); Check("routes ranked by estimated Cr per hour",plans.Length>0?plans.Zip(plans.Skip(1)).All(pair=>pair.First.CreditsPerHour>=pair.Second.CreditsPerHour):null,$"Actual local-data candidate count: {plans.Length}. {ViewModel.TradePlanningStatus}"); Check("candidate selection sets actual plan",plans.Length>0?ReferenceEquals(ViewModel.Plan,ViewModel.SelectedTradeCandidate?.Plan):null,"Selected route must bind the same RoutePlan used for execution."); Check("existing cargo disables automatic candidate execution",!ViewModel.TradeHasExistingCargo||!ViewModel.ExecuteTradePlanCommand.CanExecute(null),ViewModel.TradeExecutionPolicy); Check("risk tiers shown without fabricated probability",plans.Length>0?ViewModel.TradeCandidates.All(candidate=>Descendants(page).OfType().Any(text=>Visible(text)&&text.Text==candidate.Risk)):null,"Risk badges use the computed heuristic tier or explicitly state insufficient information."); Check("estimates include total route and approach",plans.Length>0?plans.All(plan=>plan.IsEstimate&&plan.Minutes>=plan.DeadheadMinutes&&plan.CreditsPerHour==(plan.Minutes>0?plan.NetProfit*60/(decimal)plan.Minutes:0)):null,"Hourly profit and duration are calculated from whole-route totals, including approach to origin."); Check("planning submits no execution",!ViewModel.RunActive,"Only CalculateCommand was invoked; start, plugin, session and trade commands were never invoked by this probe."); var list=(ListView)Find("TradeRouteCandidates"); for(var index=0;index=-1&&runOrigin.Y+runCard.ActualHeight<=page.ActualHeight+1,"The scrolled page capture includes the complete execution summary and its actual message."); } else { calculationSkipped="Current local balance, free cargo, jump range, station cache or idle planning command is unavailable; no demonstration data was substituted."; Check("route calculation with actual data",null,calculationSkipped); } Check("execution metrics labels",new[]{"已完成轮数","本次已确认净利润","运行时长","当前阶段"}.All(label=>Descendants(page).OfType().Any(text=>text.Text==label)),"All four execution metrics have explicit labels."); advancedPeer.Expand();scroller.ChangeView(null,0,null,true);page.UpdateLayout();await Task.Delay(160); editLayout=Layout();await Capture("trade-planner-edit"); Check("advanced reveals optional search constraints",advancedSearch.IsExpanded&&ViewModel.TradeAdvancedEditing&&Visible(Find("AutomaticTradeSource"))&&Visible(Find("AutomaticTradeDestination"))&&Visible(Find("AutomaticTradeMode")),"The actual Expander automation peer opens the optional fields and writes the shared editing state through its two-way binding."); if(isolatedFixture) { Check("empty search fields remain optional",ViewModel.TradeSourceSearchStation is null&&ViewModel.TradeDestinationSearchStation is null &&string.IsNullOrWhiteSpace(ViewModel.TradeSourceSearchText)&&string.IsNullOrWhiteSpace(ViewModel.TradeDestinationSearchText) &&string.IsNullOrWhiteSpace(ViewModel.TradeSourceSystemText)&&string.IsNullOrWhiteSpace(ViewModel.TradeDestinationSystemText),"Fresh isolated defaults leave all four source/destination constraints empty; no execution-station choice is forced into search."); var savedMode=ViewModel.TradePlanningModeIndex; try { ViewModel.AutomaticModeIndex=1;scroller.ChangeView(null,0,null,true);page.UpdateLayout();await Task.Delay(100); await Capture("trade-planner-cg-advanced"); Check("advanced CG selection updates main mode",ViewModel.TradePlanningModeIndex==1&&planningMode.SelectedIndex==1,"Changing the advanced mode updates the main choice; neither search origin nor destination must be selected."); Find("AutomaticTradeSource").StartBringIntoView(new BringIntoViewOptions{AnimationDesired=false});await Task.Delay(120); await Capture("trade-planner-cg-constraints"); Check("CG commodity filter is optional search input",Visible(Find("TradeCommoditySearchInput"))&&string.IsNullOrWhiteSpace(ViewModel.TradeCommoditySearchText),"The isolated default leaves the search commodity blank; the actual goal catalogue remains the acceptance whitelist."); Find("TradeCommoditySearchInput").StartBringIntoView(new BringIntoViewOptions{AnimationDesired=false});await Task.Delay(120); await Capture("trade-planner-cg-commodity"); } finally{ViewModel.TradePlanningModeIndex=savedMode;page.UpdateLayout();} } var timingOptions=Descendants(advanced).OfType().Single();timingOptions.IsExpanded=true; page.UpdateLayout();await Task.Delay(120); timingOptions.StartBringIntoView(new BringIntoViewOptions{AnimationDesired=false});await Task.Delay(160); await Capture("trade-planner-edit-bottom");editBottomLayout=Layout(); var timingOrigin=timingOptions.TransformToVisual(page).TransformPoint(new(0,0)); Check("expanded advanced bottom fully in viewport",timingOrigin.Y>=-1&&timingOrigin.Y+timingOptions.ActualHeight<=page.ActualHeight+1&&scroller.ScrollableWidth<1,"Expanded time, cost and station filters are visible in a scrolled actual page capture without horizontal scrolling."); timingOptions.IsExpanded=false; Check("planning does not change session or cargo",player.CommanderId==ViewModel.CurrentPlayer.CommanderId&&player.CurrentShipId==ViewModel.CurrentPlayer.CurrentShipId&&player.CargoCount==ViewModel.CurrentPlayer.CargoCount&&player.Credits==ViewModel.CurrentPlayer.Credits,"Original local commander, ship, cargo count and wallet remain unchanged during this read-only planning probe."); if(isolatedFixture)planningVisualFixture=await RunTradePlanningFixtureProbeAsync(page,directory,Capture,Check); } catch(Exception error){failure=error.ToString();} finally { advancedSearch.IsExpanded=false;scroller.ChangeView(null,0,null,true);page.UpdateLayout(); var marketStatistics=Runtime.Markets.Statistics(); await File.WriteAllTextAsync(Path.Combine(directory,"trade-planner-evidence.json"),JsonSerializer.Serialize(new { at=DateTimeOffset.UtcNow,checks,captures,defaultLayout,plannedLayout,editLayout,editBottomLayout,executionLayout,calculationSkipped,failure,regionFixture,planningVisualFixture, planningDiagnostic=ViewModel.TradePlanningDiagnosticForProbe,communityGoalRead=ViewModel.TradeCommunityGoalReadForProbe,planningProgress=ViewModel.TradeProgressForProbe, sourceKinds=ViewModel.TradeSourceKinds.Select(option=>new{option.Label,option.Kind,option.IsSelected}),ViewModel.TradeShipPadSummary,ViewModel.TradeEffectivePadSummary, actualDataDirectory=Runtime.DataDirectory,marketDatabasePath=Runtime.Markets.DatabasePath, marketStatistics=new{marketStatistics.Stations,marketStatistics.Quotes,marketStatistics.Latest},initialInvestment, candidateCount=ViewModel.TradeCandidates.Count,advancedRestoredCollapsed=!ViewModel.TradeAdvancedEditing, executionPolicy=ViewModel.TradeExecutionPolicy,executeEnabled=ViewModel.ExecuteTradePlanCommand.CanExecute(null),ViewModel.TradeHasExistingCargo, currentPlayer=new{ViewModel.CurrentPlayer.CommanderId,ViewModel.CurrentPlayer.Ship,ViewModel.CurrentPlayer.CargoCount,ViewModel.CurrentPlayer.CargoCapacity,ViewModel.CurrentPlayer.Credits}, routes=ViewModel.TradeCandidates.Select(candidate=>new{candidate.Rank,candidate.IsCommunityGoal,candidate.CommunityGoalLabel,candidate.CommunityGoalDescription,candidate.Plan.CreditsPerHour,candidate.Plan.NetProfit,candidate.Plan.Minutes,candidate.Plan.RiskDisplay,candidate.RouteSummary,candidate.Cargo,candidate.CargoDetails,candidate.Plan.Request.InvestmentCredits,candidate.Plan.IsCargoAlternative,candidate.Plan.Request.OccupiedCargo, distanceLy=candidate.Plan.DeadheadDistanceLy+candidate.Plan.Legs.Sum(leg=>leg.DistanceLy),jumps=candidate.Plan.DeadheadJumps+candidate.Plan.Legs.Sum(leg=>leg.EstimatedJumps), legs=candidate.Plan.Legs.Select(leg=>new{leg.Number,from=leg.From.Name,to=leg.To.Name,orders=leg.Orders.Select(order=>new{order.Commodity,order.Name,order.Quantity})})}), structurePassed=failure is null&&checks.All(check=>check.Status!="failed"),visualReview="Pixel and local-detail inspection is still required; capture success does not establish visual acceptance.", gameFlightCommandsInvoked=false,gameRestartInvoked=false,tradeOrdersSubmitted=0,osInputsSent=0,syntheticPlayerData=false, syntheticPlayerOrMarketData=planningVisualFixture is not null,syntheticMarketVisualFixture=planningVisualFixture is not null,syntheticRegionFixture=isolatedFixture },new JsonSerializerOptions{WriteIndented=true})); } return new{checks,captures,calculationSkipped,failure,candidateCount=ViewModel.TradeCandidates.Count,advancedRestoredCollapsed=!ViewModel.TradeAdvancedEditing, structurePassed=failure is null&&checks.All(check=>check.Status!="failed"),gameActionsInvoked=false}; } }