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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Xml.Linq;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Notifications;
using XFEEDToolBox.Core.Session;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public class SessionReadyNotificationTests
{
    private static readonly DateTimeOffset At=DateTimeOffset.Parse("2026-10-23T06:00:00Z"),ReadyAt=At.AddSeconds(20);
    private static readonly GameSessionTarget Old=new(1,At.AddMinutes(-5).UtcTicks,"C:/game/EliteDangerous64.exe","launcher",false);
    private static readonly GameSessionTarget New=Old with{ProcessId=2,StartedUtcTicks=At.AddSeconds(5).UtcTicks};
    private static PlayerState Before()=>new(){CommanderId="F1",InGame=true,LoadGameAt=At.AddMinutes(-1),SystemName="Sol",Navigation=new(){CurrentSystemAddress=42}};
    private static PlayerState Ready()=>Before() with{LoadGameAt=ReadyAt,LocationAt=ReadyAt,StatusAt=ReadyAt,StatusReadAt=ReadyAt,
        Status=new(1L<<24,0,1,1,0,null,null,null,null,[])};
    private static SessionReentryEvent Request(SessionOperation operation=SessionOperation.Restart,bool protection=false)
        =>new(Guid.NewGuid(),operation,Old,protection,SessionReentryStage.Requested,At);
    private static SessionReadyNotificationObserver Waiting(SessionReentryEvent? request=null)
    {
        var observer=new SessionReadyNotificationObserver();var entry=request??Request();observer.OnReentry(entry,Before());
        observer.OnReentry(entry with{Stage=SessionReentryStage.ProcessReady,Target=New},Before());return observer;
    }
    [Test] public void ANewProcessCannotNotifyUntilTheFreshPlayableSessionAndLocationAreReady()
    {
        var observer=new SessionReadyNotificationObserver();var request=Request();
        Check.That(observer.Observe(Ready(),New,ReadyAt) is null);
        observer.OnReentry(request,Before());Check.That(observer.Observe(Ready(),New,ReadyAt) is null);
        observer.OnReentry(request with{Stage=SessionReentryStage.ProcessReady,Target=New},Before());
        Check.That(observer.Observe(Before(),New,ReadyAt) is null);
        var notice=observer.Observe(Ready(),New,ReadyAt)!;Check.Equal("已重新进入游戏",notice.Title);
        Check.Equal("一键大退已完成 · Sol",notice.Detail);Check.That(observer.Observe(Ready(),New,ReadyAt) is null);
    }
    [Test] public void RelogRestartStartAndProtectionUseTheSameConfirmedReadinessGate()
    {
        foreach(var operation in Enum.GetValues<SessionOperation>())
        {
            var notice=Waiting(Request(operation)).Observe(Ready(),New,ReadyAt)!;
            Check.Equal(operation==SessionOperation.Start?"游戏已准备就绪":"已重新进入游戏",notice.Title);
        }
        Check.Equal("飞船保护大退已完成 · Sol",Waiting(Request(protection:true)).Observe(Ready(),New,ReadyAt)!.Detail);
    }
    [Test] public void LoadingOldStatusOldLocationMissingWorldAndUnfreshReadsDoNotCountAsEntry()
    {
        foreach(var player in new[]{Ready() with{InGame=false},Ready() with{LoadGameAt=Before().LoadGameAt},
            Ready() with{LocationAt=At},Ready() with{StatusAt=At},Ready() with{StatusReadAt=At},Ready() with{Status=null},
            Ready() with{Status=Ready().Status! with{Flags=0}},Ready() with{Navigation=new(){CurrentSystemAddress=0}},
            Ready() with{Navigation=new(){CurrentSystemAddress=42,JumpInProgress=true}}})
            Check.That(Waiting().Observe(player,New,ReadyAt) is null);
    }
    [Test] public void AnotherProcessOrCommanderCannotCompleteTheRequestedEntry()
    {
        foreach(var target in new[]{Old,New with{StartedUtcTicks=0},New with{Executable="other"}})
            Check.That(Waiting().Observe(Ready(),target,ReadyAt) is null);
        var observer=Waiting();Check.That(observer.Observe(Ready() with{CommanderId="F2"},New,ReadyAt) is null);
        Check.That(observer.Observe(Ready(),New,ReadyAt) is null);
    }
    [Test] public void AbortedOperationsAndDelayedSignalsFromPreviousOperationsCannotNotify()
    {
        var first=Request();var second=Request(SessionOperation.Relog);var observer=Waiting(first);
        observer.OnReentry(first with{Stage=SessionReentryStage.Aborted},Before());
        observer.OnReentry(first with{Stage=SessionReentryStage.ProcessReady,Target=New},Before());
        Check.That(observer.Observe(Ready(),New,ReadyAt) is null);
        observer.OnReentry(second,Before());observer.OnReentry(first with{Stage=SessionReentryStage.Aborted},Before());
        observer.OnReentry(second with{Stage=SessionReentryStage.ProcessReady,Target=New},Before());
        Check.That(observer.Observe(Ready(),New,ReadyAt)!.Detail.StartsWith("一键重进"));
    }
    [Test] public void SessionToastPreservesTextAndOnlyOpensTheHomePage()
    {
        var notice=Waiting().Observe(Ready(),New,ReadyAt)! with{Detail="A & <B>"};
        var toast=XElement.Parse(ArrivalToastContent.Build(notice));Check.Equal("action=session&page=home",toast.Attribute("launch")!.Value);
        Check.Equal("A & <B>",toast.Descendants("text").Last().Value);Check.That(!toast.Descendants("image").Any());
    }
    [Test] public async Task SessionEventsIdentifyRealRelaunchesAndObserverFailuresCannotBlockThem()
    {
        var driver=new Driver();var controller=new GameSessionController(driver,()=>false);var events=new List<SessionReentryEvent>();
        controller.Reentry+=_=>throw new InvalidOperationException("notification unavailable");controller.Reentry+=events.Add;
        await controller.RunAsync(SessionOperation.Restart);
        Check.Equal("kill,launch",string.Join(',',driver.Calls));Check.Equal(2,events.Count);
        Check.Equal(SessionReentryStage.Requested,events[0].Stage);Check.Equal(SessionReentryStage.ProcessReady,events[1].Stage);
        Check.Equal(events[0].Id,events[1].Id);Check.Equal(New.ProcessId,events[1].Target.ProcessId);
        events.Clear();await controller.RunAsync(SessionOperation.Start);Check.Equal(0,events.Count);
    }
    [Test] public async Task FailedLaunchPublishesAbortRatherThanProcessReady()
    {
        var controller=new GameSessionController(new Driver{FailLaunch=true},()=>false);var events=new List<SessionReentryEvent>();controller.Reentry+=events.Add;
        try{await controller.RunAsync(SessionOperation.Restart);}catch(InvalidOperationException){}
        Check.Equal(2,events.Count);Check.Equal(SessionReentryStage.Aborted,events[1].Stage);
    }
    private sealed class Driver:IGameSessionDriver
    {
        public bool CanRelog=>true;public bool FailLaunch;public readonly List<string> Calls=[];
        public Task<GameSessionTarget> FindTargetAsync(CancellationToken token)=>Task.FromResult(Old);
        public Task<GameSessionTarget> FindLaunchTargetAsync(CancellationToken token)=>Task.FromResult(Old);
        public Task ValidateLaunchAsync(GameSessionTarget target,CancellationToken token)=>Task.CompletedTask;
        public Task ValidateRestartAsync(GameSessionTarget target,CancellationToken token)=>Task.CompletedTask;
        public Task TerminateAsync(GameSessionTarget target,CancellationToken token){Calls.Add("kill");return Task.CompletedTask;}
        public Task<bool> HasExitedAsync(GameSessionTarget target,CancellationToken token)=>Task.FromResult(true);
        public Task LaunchAsync(GameSessionTarget target,CancellationToken token)
        {Calls.Add("launch");return FailLaunch?Task.FromException(new InvalidOperationException("launcher failed")):Task.CompletedTask;}
        public Task<GameSessionTarget?> FindReplacementAsync(GameSessionTarget previous,CancellationToken token)=>Task.FromResult<GameSessionTarget?>(New);
        public Task RelogAsync(GameSessionTarget target,CancellationToken token)=>Task.CompletedTask;
    }
}