Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions Assets/Tests/Editor/ExternalSceneChangeResolverTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEditor.SceneManagement;
using UnityEngine;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

Expand Down Expand Up @@ -190,12 +192,227 @@ public void ResolveExternalSceneChanges_WhenSceneUnchanged_DoesNotSaveOrReload()
Assert.That(reloadWasCalled, Is.False);
}

[Test]
public void FocusReturnService_WhenFocusIsLost_HoldsAutoRefreshOnce()
{
// Verifies focus loss suspends Unity Auto Refresh only once per unfocused interval.
bool autoRefreshHeld = false;
int disallowCallCount = 0;
int allowCallCount = 0;
ExternalAssetFocusReturnService service = CreateFocusReturnService(
() => autoRefreshHeld,
isHeld => autoRefreshHeld = isHeld,
() => false,
() => disallowCallCount++,
() => allowCallCount++,
() => { });

service.HandleFocusChanged(false);
service.HandleFocusChanged(false);

Assert.That(autoRefreshHeld, Is.True);
Assert.That(disallowCallCount, Is.EqualTo(1));
Assert.That(allowCallCount, Is.EqualTo(0));
}

[Test]
public void FocusReturnService_WhenFocusReturns_RunsPreflightBeforeReleasingAutoRefresh()
{
// Verifies focus return resolves editor state before Unity Auto Refresh resumes.
bool autoRefreshHeld = true;
List<string> events = new List<string>();
ExternalAssetFocusReturnService service = CreateFocusReturnService(
() => autoRefreshHeld,
isHeld => autoRefreshHeld = isHeld,
() => true,
() => events.Add("disallow"),
() => events.Add("allow"),
() => events.Add("preflight"));

service.HandleFocusChanged(true);

Assert.That(autoRefreshHeld, Is.False);
Assert.That(events, Is.EqualTo(new[] { "preflight", "allow" }));
}

[Test]
public void FocusReturnService_WhenStartupFindsHeldAutoRefreshAndEditorIsFocused_RunsPreflightBeforeReleasingIt()
{
// Verifies focused startup recovery resolves editor state before releasing a reload-surviving hold.
bool autoRefreshHeld = true;
List<string> events = new List<string>();
ExternalAssetFocusReturnService service = CreateFocusReturnService(
() => autoRefreshHeld,
isHeld => autoRefreshHeld = isHeld,
() => true,
() => events.Add("disallow"),
() => events.Add("allow"),
() => events.Add("preflight"));

bool restoredHeldAutoRefresh = service.RestoreAutoRefreshIfHeld();

Assert.That(restoredHeldAutoRefresh, Is.True);
Assert.That(autoRefreshHeld, Is.False);
Assert.That(events, Is.EqualTo(new[] { "preflight", "allow" }));
}

[Test]
public void FocusReturnService_WhenStartupFindsHeldAutoRefreshAndEditorIsUnfocused_KeepsAutoRefreshHeld()
{
// Verifies unfocused startup recovery keeps Auto Refresh suspended until focus returns.
bool autoRefreshHeld = true;
int allowCallCount = 0;
int preflightCallCount = 0;
ExternalAssetFocusReturnService service = CreateFocusReturnService(
() => autoRefreshHeld,
isHeld => autoRefreshHeld = isHeld,
() => false,
() => { },
() => allowCallCount++,
() => preflightCallCount++);

bool restoredHeldAutoRefresh = service.RestoreAutoRefreshIfHeld();

Assert.That(restoredHeldAutoRefresh, Is.False);
Assert.That(autoRefreshHeld, Is.True);
Assert.That(allowCallCount, Is.EqualTo(0));
Assert.That(preflightCallCount, Is.EqualTo(0));
}

[Test]
public void FocusReturnService_WhenPreflightThrows_StillReleasesAutoRefresh()
{
// Verifies Auto Refresh is released even when focus-return preflight fails fast.
bool autoRefreshHeld = true;
int allowCallCount = 0;
ExternalAssetFocusReturnService service = CreateFocusReturnService(
() => autoRefreshHeld,
isHeld => autoRefreshHeld = isHeld,
() => true,
() => { },
() => allowCallCount++,
() => throw new InvalidOperationException("preflight failed"));

Assert.Throws<InvalidOperationException>(() => service.HandleFocusChanged(true));
Assert.That(autoRefreshHeld, Is.False);
Assert.That(allowCallCount, Is.EqualTo(1));
}

[Test]
public void SnapshotSessionStore_WhenSnapshotsRoundTrip_PreservesFingerprints()
{
// Verifies focus-return snapshots survive a domain reload through JSON session storage.
Dictionary<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)> snapshots =
new Dictionary<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)>(StringComparer.Ordinal);
snapshots[ScenePath] = (true, SavedTime, 10);
const string MissingScenePath = "Assets/Scenes/MissingScene.unity";
snapshots[MissingScenePath] = (false, DateTime.MinValue, 0);

string json = ExternalAssetSnapshotSessionStore.SerializeSnapshots(snapshots);
Dictionary<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)> restored =
new Dictionary<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)>(StringComparer.Ordinal);

ExternalAssetSnapshotSessionStore.RestoreSnapshots(restored, json);

Assert.That(restored.Count, Is.EqualTo(2));
Assert.That(restored[ScenePath].Exists, Is.True);
Assert.That(restored[ScenePath].LastWriteTimeUtc, Is.EqualTo(SavedTime));
Assert.That(restored[ScenePath].Length, Is.EqualTo(10));
Assert.That(restored[MissingScenePath].Exists, Is.False);
}

[Test]
public void SnapshotSessionStore_WhenJsonIsEmpty_ClearsSnapshots()
{
// Verifies empty session data clears stale snapshots after normal startup.
Dictionary<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)> snapshots = CreateSnapshots();

ExternalAssetSnapshotSessionStore.RestoreSnapshots(snapshots, "");

Assert.That(snapshots, Is.Empty);
}

[Test]
public void CreatePrefabStageReopenContext_WhenInstanceIsValid_PreservesContext()
{
// Verifies valid in-context Prefab Stage reopen data is preserved.
GameObject openedFromInstanceObject = new GameObject("OpenedFromInstanceObject");
try
{
(GameObject OpenedFromInstanceObject, PrefabStage.Mode Mode) context =
ExternalSceneChangeTracker.CreatePrefabStageReopenContext(
openedFromInstanceObject,
PrefabStage.Mode.InContext,
_ => true);

Assert.That(context.OpenedFromInstanceObject, Is.SameAs(openedFromInstanceObject));
Assert.That(context.Mode, Is.EqualTo(PrefabStage.Mode.InContext));
}
finally
{
UnityEngine.Object.DestroyImmediate(openedFromInstanceObject);
}
}

[Test]
public void CreatePrefabStageReopenContext_WhenInstanceIsInvalid_FallsBackToIsolation()
{
// Verifies invalid in-context Prefab Stage reopen data cannot reach OpenPrefab.
GameObject openedFromInstanceObject = new GameObject("OpenedFromInstanceObject");
try
{
(GameObject OpenedFromInstanceObject, PrefabStage.Mode Mode) context =
ExternalSceneChangeTracker.CreatePrefabStageReopenContext(
openedFromInstanceObject,
PrefabStage.Mode.InContext,
_ => false);

Assert.That(context.OpenedFromInstanceObject, Is.Null);
Assert.That(context.Mode, Is.EqualTo(PrefabStage.Mode.InIsolation));
}
finally
{
UnityEngine.Object.DestroyImmediate(openedFromInstanceObject);
}
}

[Test]
public void CreatePrefabStageReopenContext_WhenInstanceIsMissing_UsesIsolation()
{
// Verifies missing Prefab Stage context reopens without invalid InContext arguments.
(GameObject OpenedFromInstanceObject, PrefabStage.Mode Mode) context =
ExternalSceneChangeTracker.CreatePrefabStageReopenContext(
null,
PrefabStage.Mode.InContext,
_ => true);

Assert.That(context.OpenedFromInstanceObject, Is.Null);
Assert.That(context.Mode, Is.EqualTo(PrefabStage.Mode.InIsolation));
}

private static Dictionary<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)> CreateSnapshots()
{
Dictionary<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)> snapshots =
new Dictionary<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)>(StringComparer.Ordinal);
snapshots[ScenePath] = (true, SavedTime, 10);
return snapshots;
}

private static ExternalAssetFocusReturnService CreateFocusReturnService(
Func<bool> getAutoRefreshHeld,
Action<bool> setAutoRefreshHeld,
Func<bool> isEditorFocused,
Action disallowAutoRefresh,
Action allowAutoRefresh,
Action resolveFocusReturnChanges)
{
return new ExternalAssetFocusReturnService(
getAutoRefreshHeld,
setAutoRefreshHeld,
isEditorFocused,
disallowAutoRefresh,
allowAutoRefresh,
resolveFocusReturnChanges);
}
}
}
Loading
Loading