using System.Collections; using System.Linq; using System.Reflection; using NUnit.Framework; using Unity.Cinemachine; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; using Yarn.Unity; namespace NightclubArcadia.PlayMode.Tests { /// /// Proves the scene split actually comes up. /// /// Splitting Systems out of the level nulled seven serialized references that /// Unity cannot carry across a scene boundary — the two NPCs' dialogue runner, UI /// and player fields, and the reveal camera's tracking target. Each is supposed to /// be resolved at runtime instead. An EditMode test cannot see any of that, because /// none of it has run. This can. /// /// If this suite fails, the game does not start. Treat it that way. /// /// Two checks reach their types by name through reflection rather than referencing /// them. NPCStandIn and NightclubArcadia.Cinematics live in Assembly-CSharp, and an /// assembly-definition test assembly cannot reference the predefined assemblies — /// only the other way round. Giving the runtime code its own asmdefs /// (docs/restructure-plan.md Phase 4.1) removes the need for this; until then the /// reflection is the accommodation, and it is confined to these two tests. /// public class BootstrapTests { const string Bootstrap = "Bootstrap"; const string Systems = "Systems"; const string Level = "SC101_ConferenceHall"; [UnitySetUp] public IEnumerator LoadFromBootstrap() { yield return SceneManager.LoadSceneAsync(Bootstrap, LoadSceneMode.Single); // GameBootstrap loads the other two additively over a few frames. var deadline = Time.realtimeSinceStartup + 20f; while (Time.realtimeSinceStartup < deadline) { if (SceneManager.GetSceneByName(Systems).isLoaded && SceneManager.GetSceneByName(Level).isLoaded) { break; } yield return null; } // let Awake/Start run on everything that just loaded for (var i = 0; i < 5; i++) { yield return null; } } [Test] public void Bootstrap_LoadsSystemsAndLevel() { Assert.IsTrue(SceneManager.GetSceneByName(Systems).isLoaded, "Systems scene did not load"); Assert.IsTrue(SceneManager.GetSceneByName(Level).isLoaded, "Level scene did not load"); Assert.AreEqual(Level, SceneManager.GetActiveScene().name, "The level should be the active scene, so new objects and lighting land there"); } [Test] public void ThePlayerExists_AndIsTagged() { var player = GameObject.FindGameObjectWithTag("Player"); Assert.IsNotNull(player, "No object tagged Player — the reveal camera and the NPCs both need it"); } [Test] public void TheDialogueRunnerExists() { Assert.IsNotNull(Object.FindFirstObjectByType(), "No DialogueRunner in any loaded scene"); } [Test] public void EveryNpc_ResolvedItsDialogueRunner() { var npcs = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None) .Where(m => m != null && m.GetType().Name == "NPCStandIn") .ToArray(); Assert.IsNotEmpty(npcs, "Expected NPCs in the level scene"); foreach (var npc in npcs) { var field = npc.GetType().GetField("dialogueRunner", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); Assert.IsNotNull(field, "NPCStandIn no longer has a dialogueRunner field"); Assert.IsNotNull(field.GetValue(npc) as Object, $"'{npc.name}' did not resolve a DialogueRunner. It lives in the Systems scene, " + "so the serialized reference is gone and SceneServices.Resolve has to find it."); } } [Test] public void RevealCameras_BoundToThePlayer() { var bound = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None) .Where(cam => cam.GetComponents() .Any(m => m != null && m.GetType().Name == "CinemachineFollowsPlayer")) .ToArray(); Assert.IsNotEmpty(bound, "Expected at least one runtime-bound camera"); foreach (var cam in bound) { Assert.IsNotNull(cam.Follow, $"'{cam.name}' never bound its Follow target to the player"); } } } }