Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/MissionAchievementPanelPlayModeTests.cs
T

1041 lines
49 KiB
C#

using System.Collections;
using System.IO;
using System.Reflection;
using BumpCombat.Core;
using BumpCombat.Progression;
using BumpCombat.UI;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace BumpCombat.PlayModeTests
{
public sealed class MissionAchievementPanelPlayModeTests
{
private Keyboard virtualKeyboard;
private bool virtualKeyboardInputActive;
private InputSettings.BackgroundBehavior previousBackgroundBehavior;
private InputSettings.EditorInputBehaviorInPlayMode previousEditorInputBehavior;
private bool previousRunInBackground;
[SetUp]
public void SetUp()
{
ArtifactRewardController.GrantCatalogForTests = true;
RunManager.ForceProductionModeForTests = true;
Time.timeScale = 1f;
}
[TearDown]
public void TearDown()
{
EndVirtualKeyboardInput();
ArtifactRewardController.GrantCatalogForTests = false;
RunManager.ForceProductionModeForTests = false;
Time.timeScale = 1f;
}
[UnityTest]
public IEnumerator TabPanel_ShowsAllMissionsAndAchievementsAndClosesWithTabOrEscape()
{
yield return LoadSceneAndWait();
RunManager runManager = Object.FindAnyObjectByType<RunManager>();
MissionAchievementPanel panel =
Object.FindAnyObjectByType<MissionAchievementPanel>();
RunMissionTracker tracker =
Object.FindAnyObjectByType<RunMissionTracker>();
Assert.That(runManager, Is.Not.Null);
Assert.That(panel, Is.Not.Null);
Assert.That(tracker, Is.Not.Null);
Assert.That(panel.IsOpen, Is.False);
Assert.That(panel.PageCount, Is.EqualTo(2));
SetPrivateField(runManager, "selectedRunMode", RunMode.Production);
SetPrivateField(runManager, "hasSelectedRunMode", true);
SetPrivateField(runManager, "guardUnlocked", false);
SetPrivateField(runManager, "artifactChargeUnlocked", false);
Assert.That(panel.transform.Find("Mission List Hint").gameObject.activeSelf,
Is.True);
Text missionHint = panel.transform.Find("Mission List Hint")
.GetComponent<Text>();
RectTransform missionHintRect = missionHint.rectTransform;
Assert.That(missionHint.supportRichText, Is.True);
Assert.That(missionHint.resizeTextForBestFit, Is.False);
Assert.That(missionHint.alignment, Is.EqualTo(TextAnchor.MiddleLeft));
Assert.That(missionHint.text, Does.Contain("<size=36>"));
Assert.That(missionHint.text, Does.Contain("<size=24>(미션 / 업적)</size>"));
Assert.That(missionHintRect.anchorMin, Is.EqualTo(new Vector2(0f, 1f)));
Assert.That(missionHintRect.anchorMax, Is.EqualTo(new Vector2(0f, 1f)));
Assert.That(missionHintRect.pivot, Is.EqualTo(new Vector2(0f, 1f)));
Assert.That(missionHintRect.anchoredPosition,
Is.EqualTo(new Vector2(24f, -250f)));
Assert.That(missionHintRect.sizeDelta,
Is.EqualTo(new Vector2(220f, 110f)));
Keyboard keyboard = BeginVirtualKeyboardInput();
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.True);
Assert.That(runManager.IsSelectionOpen, Is.True);
Assert.That(Time.timeScale, Is.Zero);
Assert.That(panel.VisibleMissionRowCount, Is.EqualTo(6));
Assert.That(panel.transform.Find("Mission List Dim")
.GetComponent<Image>().raycastTarget, Is.True);
Transform firstRow = panel.transform.Find(
"Mission and Achievement List/Mission List Page/Mission Row 1");
Text firstMission = firstRow.Find("Mission Title")
.GetComponent<Text>();
Assert.That(firstMission.text, Is.EqualTo("첫 소탕"));
Assert.That(firstRow.Find("Mission Condition").GetComponent<Text>().text,
Does.Contain("일반 적 100마리 처치"));
Assert.That(firstRow.Find("Mission Condition").GetComponent<Text>().text,
Does.Contain("소환수 제외"));
Assert.That(firstRow.Find("Mission Progress").GetComponent<Text>().text,
Is.EqualTo("0/100"));
Assert.That(firstRow.Find("Mission Reward").GetComponent<Text>().text,
Is.EqualTo("+10 XP"));
Assert.That(firstRow.Find("Mission Status Badge").gameObject.activeSelf,
Is.False, "Uncompleted missions do not repeat a '진행 중' badge.");
yield return PressAndRelease(keyboard, Key.PageDown);
Assert.That(panel.CurrentPageIndex, Is.EqualTo(1));
Assert.That(panel.VisibleMissionRowCount, Is.EqualTo(5));
Assert.That(panel.transform.Find(
"Mission and Achievement List/Mission Page Number").GetComponent<Text>().text,
Is.EqualTo("2/2"));
Transform finalRow = panel.transform.Find(
"Mission and Achievement List/Mission List Page/Mission Row 5");
Text finalMission = finalRow.Find("Mission Title")
.GetComponent<Text>();
Assert.That(finalMission.text, Does.Contain("파죽지세"));
Assert.That(finalRow.Find("Mission Condition").GetComponent<Text>().text,
Does.Contain("소환수 제외"));
Transform lockedRow = panel.transform.Find(
"Mission and Achievement List/Mission List Page/Mission Row 2");
Text lockedMission = lockedRow.Find("Mission Title")
.GetComponent<Text>();
Assert.That(lockedMission.text, Does.Contain("정확한 방어"));
Assert.That(lockedRow.Find("Mission Status Badge/Label")
.GetComponent<Text>().text, Is.EqualTo("해금 대기"));
Assert.That(lockedRow.Find("Mission Progress").GetComponent<Text>().text,
Is.EqualTo("0/3"));
Assert.That(lockedRow.Find("Mission Reward").GetComponent<Text>().text,
Is.EqualTo("+15 XP"));
yield return PressAndRelease(keyboard, Key.PageUp);
Assert.That(panel.CurrentPageIndex, Is.Zero);
panel.transform.Find("Mission and Achievement List/Next Mission Page")
.GetComponent<Button>().onClick.Invoke();
Assert.That(panel.CurrentPageIndex, Is.EqualTo(1),
"The visible page button should navigate to the next mission page.");
panel.transform.Find("Mission and Achievement List/Achievement Tab")
.GetComponent<Button>().onClick.Invoke();
Assert.That(panel.ShowingAchievements, Is.True);
Assert.That(panel.CurrentPageIndex, Is.EqualTo(1),
"Switching tabs should preserve the selected mission page.");
Assert.That(panel.transform.Find(
"Mission and Achievement List/Achievement List Page/Achievement Preparation Message")
.GetComponent<Text>().text, Does.Contain("업적 준비 중"));
Assert.That(panel.transform.Find(
"Mission and Achievement List/Achievement List Page/Lifetime Mission Records")
.GetComponent<Text>().text, Does.Contain("누적 미션 완료"));
Assert.That(panel.transform.Find(
"Mission and Achievement List/Achievement List Page/Lifetime Mission Records")
.GetComponent<Text>().text, Does.Not.Contain("스킨"));
Transform missionTab = panel.transform.Find(
"Mission and Achievement List/Mission Tab");
Transform achievementTab = panel.transform.Find(
"Mission and Achievement List/Achievement Tab");
Assert.That(missionTab.Find("Label").GetComponent<Text>().text,
Is.EqualTo("미션 [1]"));
Assert.That(achievementTab.Find("Label").GetComponent<Text>().text,
Is.EqualTo("업적 [2]"));
Assert.That(panel.transform.Find(
"Mission and Achievement List/Mission List Help")
.GetComponent<Text>().text, Does.Contain("← / → 탭"));
Assert.That(panel.transform.Find(
"Mission and Achievement List/Mission List Help")
.GetComponent<Text>().text, Does.Contain("↑ / ↓ 페이지"));
yield return PressAndRelease(keyboard, Key.LeftArrow);
Assert.That(panel.ShowingAchievements, Is.False,
"Left arrow should select the mission tab.");
Assert.That(panel.CurrentPageIndex, Is.EqualTo(1));
yield return PressAndRelease(keyboard, Key.RightArrow);
Assert.That(panel.ShowingAchievements, Is.True,
"Right arrow should select the achievement tab.");
yield return PressAndRelease(keyboard, Key.LeftArrow);
Assert.That(panel.ShowingAchievements, Is.False,
"The mission list should be reachable again from achievements.");
Assert.That(panel.CurrentPageIndex, Is.EqualTo(1));
yield return PressAndRelease(keyboard, Key.Digit1);
Assert.That(panel.ShowingAchievements, Is.False);
yield return PressEscapeAndAssertPauseIsSuppressedThisFrame(
keyboard,
panel,
runManager);
Assert.That(panel.IsOpen, Is.False);
Assert.That(runManager.IsSelectionOpen, Is.False);
Assert.That(panel.transform.Find("Mission List Hint").gameObject.activeSelf,
Is.True);
yield return null;
Assert.That(runManager.TryOpenPause(), Is.True);
Assert.That(runManager.ClosePause(), Is.True);
}
[UnityTest]
public IEnumerator MissionCompletionToast_QueuesAndFreezesDuringMenus()
{
yield return LoadSceneAndWait();
RunManager runManager = Object.FindAnyObjectByType<RunManager>();
MissionAchievementPanel panel =
Object.FindAnyObjectByType<MissionAchievementPanel>();
RunMissionTracker tracker =
Object.FindAnyObjectByType<RunMissionTracker>();
RunMenuController menu = Object.FindAnyObjectByType<RunMenuController>();
Assert.That(runManager, Is.Not.Null);
Assert.That(panel, Is.Not.Null);
Assert.That(tracker, Is.Not.Null);
Assert.That(menu, Is.Not.Null);
SetPrivateField(runManager, "selectedRunMode", RunMode.Production);
SetPrivateField(runManager, "hasSelectedRunMode", true);
GameObject toast = GetPrivateField<GameObject>(panel, "completionToast");
Assert.That(toast, Is.Not.Null);
Assert.That(toast.activeSelf, Is.False);
Assert.That(toast.GetComponent<Image>().raycastTarget, Is.False);
CanvasGroup canvasGroup = toast.GetComponent<CanvasGroup>();
Assert.That(canvasGroup.blocksRaycasts, Is.False);
Assert.That(canvasGroup.interactable, Is.False);
foreach (Graphic graphic in toast.GetComponentsInChildren<Graphic>(true))
{
Assert.That(graphic.raycastTarget, Is.False);
}
CompleteMissionForToast(tracker, panel, 0);
CompleteMissionForToast(tracker, panel, 1);
CompleteMissionForToast(tracker, panel, 0);
Assert.That(GetQueuedCompletionNoticeCount(panel), Is.EqualTo(2),
"A repeated completion notification must not add another queue item.");
Keyboard keyboard = BeginVirtualKeyboardInput();
Assert.That(runManager.TryOpenSelection(menu), Is.True);
yield return null;
Assert.That(toast.activeSelf, Is.False,
"Queued notices stay hidden during another selection screen.");
Assert.That(GetQueuedCompletionNoticeCount(panel), Is.EqualTo(2));
Assert.That(runManager.CloseSelection(menu), Is.True);
yield return null;
Assert.That(toast.activeSelf, Is.True);
Assert.That(GetPrivateField<Text>(panel, "completionToastMissionText").text,
Is.EqualTo("첫 소탕"));
Assert.That(GetPrivateField<Text>(panel, "completionToastRewardText").text,
Is.EqualTo("+10 XP"));
float initialRemaining =
GetPrivateField<float>(panel, "activeCompletionNoticeRemaining");
Assert.That(initialRemaining, Is.GreaterThan(2.5f));
Assert.That(initialRemaining, Is.LessThanOrEqualTo(3f));
yield return new WaitForSeconds(0.2f);
float remainingAfterVisibleTime =
GetPrivateField<float>(panel, "activeCompletionNoticeRemaining");
Assert.That(remainingAfterVisibleTime, Is.LessThan(initialRemaining));
Assert.That(runManager.TryOpenPause(), Is.True);
yield return null;
Assert.That(toast.activeSelf, Is.False,
"The toast must hide as soon as pause opens.");
float remainingAtPause =
GetPrivateField<float>(panel, "activeCompletionNoticeRemaining");
yield return new WaitForSecondsRealtime(0.2f);
Assert.That(GetPrivateField<float>(panel, "activeCompletionNoticeRemaining"),
Is.EqualTo(remainingAtPause).Within(0.001f),
"Paused real time must not consume the toast duration.");
Assert.That(runManager.ClosePause(), Is.True);
yield return null;
Assert.That(toast.activeSelf, Is.True);
float remainingAtPanelOpen =
GetPrivateField<float>(panel, "activeCompletionNoticeRemaining");
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.True);
Assert.That(toast.activeSelf, Is.False,
"The toast must hide under the mission panel.");
Assert.That(GetPrivateField<float>(panel, "activeCompletionNoticeRemaining"),
Is.EqualTo(remainingAtPanelOpen).Within(0.05f));
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.False);
yield return null;
Assert.That(toast.activeSelf, Is.True,
"The active toast should resume after the panel closes.");
SetPrivateField(panel, "activeCompletionNoticeRemaining", 0f);
yield return null;
Assert.That(toast.activeSelf, Is.False,
"The first toast should end when its three gameplay seconds expire.");
yield return null;
Assert.That(toast.activeSelf, Is.True);
Assert.That(GetPrivateField<Text>(panel, "completionToastMissionText").text,
Is.EqualTo("성장의 발판"),
"The next queued completion should appear after the first one ends.");
Assert.That(GetQueuedCompletionNoticeCount(panel), Is.Zero);
}
[UnityTest]
public IEnumerator OtherModalTitlePauseAndGameOver_DoNotLeaveMissionPanelOpen()
{
yield return LoadSceneAndWait();
RunManager runManager = Object.FindAnyObjectByType<RunManager>();
MissionAchievementPanel panel =
Object.FindAnyObjectByType<MissionAchievementPanel>();
RunMissionTracker tracker =
Object.FindAnyObjectByType<RunMissionTracker>();
RunMenuController menu = Object.FindAnyObjectByType<RunMenuController>();
Assert.That(runManager, Is.Not.Null);
Assert.That(panel, Is.Not.Null);
Assert.That(menu, Is.Not.Null);
Keyboard keyboard = BeginVirtualKeyboardInput();
Assert.That(runManager.TryOpenSelection(menu), Is.True);
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.False);
Assert.That(runManager.IsSelectionOpen, Is.True,
"The mission panel must not take another screen's selection lock.");
Assert.That(runManager.CloseSelection(menu), Is.True);
Assert.That(runManager.TryOpenPause(), Is.True);
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.False);
Assert.That(runManager.IsPaused, Is.True);
Assert.That(runManager.ClosePause(), Is.True);
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.True);
CompleteMissionForToast(tracker, panel, 0);
Assert.That(GetQueuedCompletionNoticeCount(panel), Is.EqualTo(1));
runManager.EndRun();
Assert.That(panel.IsOpen, Is.False);
Assert.That(runManager.IsSelectionOpen, Is.False);
Assert.That(runManager.IsGameOver, Is.True);
Assert.That(GetPrivateField<GameObject>(panel, "completionToast").activeSelf,
Is.False);
Assert.That(GetQueuedCompletionNoticeCount(panel), Is.Zero,
"Game over must clear the active toast and pending queue.");
}
[UnityTest]
public IEnumerator TitleScreen_DoesNotOpenTheMissionPanel()
{
ArtifactRewardController.GrantCatalogForTests = false;
yield return LoadSceneAndWait();
RunManager runManager = Object.FindAnyObjectByType<RunManager>();
MissionAchievementPanel panel =
Object.FindAnyObjectByType<MissionAchievementPanel>();
RunMenuController menu = Object.FindAnyObjectByType<RunMenuController>();
Assert.That(runManager, Is.Not.Null);
Assert.That(panel, Is.Not.Null);
Assert.That(menu, Is.Not.Null);
Assert.That(runManager.IsTitleScreen, Is.True);
Keyboard keyboard = BeginVirtualKeyboardInput();
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.False);
Assert.That(runManager.IsTitleScreen, Is.True);
yield return PressAndRelease(keyboard, Key.DownArrow);
yield return PressAndRelease(keyboard, Key.DownArrow);
Assert.That(
EventSystem.current.currentSelectedGameObject
.GetComponentInChildren<Text>().text,
Is.EqualTo("업적"));
yield return PressAndRelease(keyboard, Key.Enter);
Assert.That(panel.IsOpen, Is.True);
Assert.That(panel.ShowingAchievements, Is.True);
Assert.That(runManager.IsSelectionOpen, Is.False,
"Title records do not acquire the gameplay selection lock.");
Assert.That(runManager.IsTitleScreen, Is.True);
Assert.That(Time.timeScale, Is.Zero);
Transform achievementPanel = panel.transform.Find(
"Mission and Achievement List");
Assert.That(achievementPanel.Find("Mission Tab").gameObject.activeSelf,
Is.False,
"Title records do not expose current-run missions.");
Assert.That(achievementPanel.Find("Mission List Page").gameObject.activeSelf,
Is.False);
Assert.That(achievementPanel.Find("Achievement List Page").gameObject.activeSelf,
Is.True);
Text records = achievementPanel.Find(
"Achievement List Page/Lifetime Mission Records").GetComponent<Text>();
Assert.That(records.text, Does.Contain("누적 미션 완료"));
Assert.That(achievementPanel.Find("Mission List Title")
.GetComponent<Text>().text, Is.EqualTo("업적"));
yield return PressAndRelease(keyboard, Key.Digit1);
Assert.That(panel.ShowingAchievements, Is.True,
"Keyboard tab changes cannot switch title records to missions.");
yield return PressAndRelease(keyboard, Key.Escape);
Assert.That(panel.IsOpen, Is.False);
Assert.That(menu.IsVisible, Is.True);
Assert.That(runManager.IsSelectionOpen, Is.False);
Assert.That(runManager.IsTitleScreen, Is.True);
Assert.That(Time.timeScale, Is.Zero);
Assert.That(menu.transform.Find("Run Menu/Button 1/Label")
.GetComponent<Text>().text, Is.EqualTo("게임시작"));
menu.transform.Find("Run Menu/Button 3").GetComponent<Button>()
.onClick.Invoke();
Assert.That(panel.IsOpen, Is.True);
panel.enabled = false;
Assert.That(panel.IsOpen, Is.False);
Assert.That(menu.IsVisible, Is.True,
"Disabling the title records panel returns to the title menu.");
Assert.That(runManager.IsSelectionOpen, Is.False);
panel.enabled = true;
yield return null;
Assert.That(panel.IsOpen, Is.False);
Assert.That(menu.IsVisible, Is.True);
}
[UnityTest]
public IEnumerator AchievementRecords_ExplainUnavailableOrUnpersistedProfileState()
{
yield return LoadSceneAndWait();
MissionAchievementPanel panel =
Object.FindAnyObjectByType<MissionAchievementPanel>();
RunMissionTracker tracker =
Object.FindAnyObjectByType<RunMissionTracker>();
Assert.That(panel, Is.Not.Null);
Assert.That(tracker, Is.Not.Null);
UserProfileStore store = tracker.ProfileStore;
SetPrivateField(store, "<LoadIssue>k__BackingField", "save-failed");
Keyboard keyboard = BeginVirtualKeyboardInput();
yield return PressAndRelease(keyboard, Key.Tab);
Button achievementTab = panel.transform.Find(
"Mission and Achievement List/Achievement Tab").GetComponent<Button>();
achievementTab.onClick.Invoke();
Text records = panel.transform.Find(
"Mission and Achievement List/Achievement List Page/Lifetime Mission Records")
.GetComponent<Text>();
Assert.That(records.text, Does.Contain("누적 미션 완료"));
Assert.That(records.text, Does.Contain("영구 저장 실패"));
Assert.That(records.text, Does.Not.Contain("save-failed"));
SetPrivateField(store, "<CanSave>k__BackingField", false);
SetPrivateField(store, "<LoadIssue>k__BackingField", "newer-schema");
achievementTab.onClick.Invoke();
Assert.That(records.text, Does.Contain("프로필 버전이 높아"));
Assert.That(records.text, Does.Not.Contain("누적 미션 완료"));
Assert.That(records.text, Does.Not.Contain("newer-schema"));
}
[UnityTest]
public IEnumerator DisableAndReenable_ReleasesOnlyItsOwnSelectionLock()
{
yield return LoadSceneAndWait();
RunManager runManager = Object.FindAnyObjectByType<RunManager>();
MissionAchievementPanel panel =
Object.FindAnyObjectByType<MissionAchievementPanel>();
RunMissionTracker tracker =
Object.FindAnyObjectByType<RunMissionTracker>();
Keyboard keyboard = BeginVirtualKeyboardInput();
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.True);
CompleteMissionForToast(tracker, panel, 0);
CompleteMissionForToast(tracker, panel, 1);
Assert.That(GetQueuedCompletionNoticeCount(panel), Is.EqualTo(2));
GameObject toast = GetPrivateField<GameObject>(panel, "completionToast");
panel.enabled = false;
Assert.That(panel.IsOpen, Is.False);
Assert.That(runManager.IsSelectionOpen, Is.False);
Assert.That(RunManager.GameplayInputEnabled, Is.True);
Assert.That(toast.activeSelf, Is.False);
Assert.That(GetQueuedCompletionNoticeCount(panel), Is.Zero,
"Disabling the panel must clear active and queued notices.");
panel.enabled = true;
yield return null;
Assert.That(panel.IsOpen, Is.False,
"Reenabling the component must not reopen a previously held modal.");
Assert.That(runManager.IsSelectionOpen, Is.False);
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.True);
Assert.That(runManager.IsSelectionOpen, Is.True);
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.False);
CompleteMissionForToast(tracker, panel, 2);
yield return null;
Assert.That(toast.activeSelf, Is.True);
Object.Destroy(panel);
yield return null;
Assert.That(toast.activeSelf, Is.False,
"Destroying the component must hide its toast child.");
}
[UnityTest]
public IEnumerator LayoutAt720pAnd1080p_StaysInsideCanvasAndCapturesScreenshots()
{
yield return LoadSceneAndWait();
RunManager runManager = Object.FindAnyObjectByType<RunManager>();
MissionAchievementPanel panel =
Object.FindAnyObjectByType<MissionAchievementPanel>();
RunMissionTracker tracker =
Object.FindAnyObjectByType<RunMissionTracker>();
Assert.That(runManager, Is.Not.Null);
Assert.That(panel, Is.Not.Null);
Assert.That(tracker, Is.Not.Null);
SetPrivateField(runManager, "selectedRunMode", RunMode.Production);
SetPrivateField(runManager, "hasSelectedRunMode", true);
SetPrivateField(runManager, "guardUnlocked", false);
SetPrivateField(runManager, "artifactChargeUnlocked", false);
int[] progressValues = GetPrivateField<int[]>(tracker, "progress");
bool[] completedValues = GetPrivateField<bool[]>(tracker, "completed");
progressValues[0] = 37;
progressValues[2] = 60;
completedValues[2] = true;
Keyboard keyboard = BeginVirtualKeyboardInput();
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.True);
InvokePrivateMethod(panel, "RefreshView");
Canvas canvas = Object.FindAnyObjectByType<Canvas>();
CanvasScaler scaler = canvas.GetComponent<CanvasScaler>();
Assert.That(scaler.referenceResolution,
Is.EqualTo(new Vector2(1920f, 1080f)));
RectTransform panelRect = panel.transform.Find(
"Mission and Achievement List").GetComponent<RectTransform>();
Assert.That(panelRect.sizeDelta,
Is.EqualTo(new Vector2(1360f, 880f)));
Assert.That(panelRect.rect.width * (720f / 1080f),
Is.LessThan(1280f));
Assert.That(panelRect.rect.height * (720f / 1080f),
Is.LessThan(720f));
Transform page1 = panel.transform.Find(
"Mission and Achievement List/Mission List Page");
Transform partialRow = page1.Find("Mission Row 1");
Transform completedRow = page1.Find("Mission Row 3");
Assert.That(partialRow.Find("Mission Progress").GetComponent<Text>().text,
Is.EqualTo("37/100"));
Assert.That(partialRow.Find("Mission Status Badge").gameObject.activeSelf,
Is.False);
Assert.That(completedRow.Find("Mission Status Badge/Label")
.GetComponent<Text>().text, Is.EqualTo("완료"));
Assert.That(completedRow.Find("Mission Progress").GetComponent<Text>().text,
Is.EqualTo("60/60"));
Text title = page1.Find("Mission Row 1/Mission Title").GetComponent<Text>();
Text condition = page1.Find("Mission Row 1/Mission Condition")
.GetComponent<Text>();
Text progressText = page1.Find("Mission Row 1/Mission Progress")
.GetComponent<Text>();
Text reward = page1.Find("Mission Row 1/Mission Reward").GetComponent<Text>();
Assert.That(title.fontSize * 720f / scaler.referenceResolution.y,
Is.GreaterThanOrEqualTo(21f));
Assert.That(progressText.fontSize * 720f / scaler.referenceResolution.y,
Is.GreaterThanOrEqualTo(21f));
Assert.That(condition.fontSize * 720f / scaler.referenceResolution.y,
Is.GreaterThanOrEqualTo(17f));
Assert.That(reward.fontSize * 720f / scaler.referenceResolution.y,
Is.GreaterThanOrEqualTo(17f));
Assert.That(title.resizeTextForBestFit, Is.False);
Assert.That(condition.resizeTextForBestFit, Is.False);
Button missionTab = panel.transform.Find(
"Mission and Achievement List/Mission Tab").GetComponent<Button>();
Image missionTabImage = missionTab.targetGraphic as Image;
Color selectedMissionColor = missionTabImage.color;
Assert.That(missionTab.transition, Is.EqualTo(Selectable.Transition.None));
Assert.That(missionTabImage.sprite, Is.Null);
Assert.That(missionTab.GetComponent<Outline>(), Is.Not.Null);
Assert.That(selectedMissionColor.r, Is.GreaterThan(0.85f));
Assert.That(selectedMissionColor.g, Is.GreaterThan(0.65f));
Assert.That(selectedMissionColor.b, Is.LessThan(0.5f));
missionTab.OnPointerEnter(new PointerEventData(EventSystem.current));
Assert.That(missionTabImage.color, Is.EqualTo(selectedMissionColor),
"Pointer hover must preserve the selected tab fill.");
string captureDirectory = Path.GetFullPath(Path.Combine(
Application.dataPath,
"..",
"..",
"output",
"mission-readability-v2",
"captures"));
Directory.CreateDirectory(captureDirectory);
string toastCaptureDirectory = Path.GetFullPath(Path.Combine(
Application.dataPath,
"..",
"..",
"output",
"mission-tab-hint-v1",
"captures"));
Directory.CreateDirectory(toastCaptureDirectory);
RectTransform finalMissionRow = page1.Find("Mission Row 6")
.GetComponent<RectTransform>();
CaptureBothResolutions(
canvas,
panelRect,
finalMissionRow,
captureDirectory,
"page1");
CaptureBothResolutions(
canvas,
panelRect,
finalMissionRow,
toastCaptureDirectory,
"mission-page1");
yield return PressAndRelease(keyboard, Key.PageDown);
Assert.That(panel.CurrentPageIndex, Is.EqualTo(1));
Transform page2 = panel.transform.Find(
"Mission and Achievement List/Mission List Page");
Assert.That(page2.Find("Mission Row 2/Mission Status Badge/Label")
.GetComponent<Text>().text, Is.EqualTo("해금 대기"));
CaptureBothResolutions(
canvas,
panelRect,
page2.Find("Mission Row 5").GetComponent<RectTransform>(),
captureDirectory,
"page2");
CaptureBothResolutions(
canvas,
panelRect,
page2.Find("Mission Row 5").GetComponent<RectTransform>(),
toastCaptureDirectory,
"mission-page2");
Button achievementTab = panel.transform.Find(
"Mission and Achievement List/Achievement Tab").GetComponent<Button>();
Assert.That(achievementTab.transition,
Is.EqualTo(Selectable.Transition.None));
achievementTab.onClick.Invoke();
Assert.That(panel.ShowingAchievements, Is.True);
Assert.That(panel.transform.Find(
"Mission and Achievement List/Mission Progress Header").gameObject.activeSelf,
Is.False);
Assert.That(panel.transform.Find(
"Mission and Achievement List/Mission Reward Header").gameObject.activeSelf,
Is.False);
Color selectedAchievementColor =
(achievementTab.targetGraphic as Image).color;
Assert.That(selectedAchievementColor.r, Is.GreaterThan(0.85f));
Assert.That(achievementTab.GetComponentInChildren<Text>().color.r,
Is.LessThan(0.3f));
achievementTab.OnPointerEnter(new PointerEventData(EventSystem.current));
Assert.That((achievementTab.targetGraphic as Image).color,
Is.EqualTo(selectedAchievementColor),
"Pointer hover must preserve the selected tab fill.");
CaptureBothResolutions(
canvas,
panelRect,
page2.Find("Mission Row 5").GetComponent<RectTransform>(),
captureDirectory,
"achievements");
CaptureBothResolutions(
canvas,
panelRect,
page2.Find("Mission Row 5").GetComponent<RectTransform>(),
toastCaptureDirectory,
"achievements");
yield return PressAndRelease(keyboard, Key.Tab);
Assert.That(panel.IsOpen, Is.False);
completedValues[2] = false;
CompleteMissionForToast(tracker, panel, 0);
yield return null;
GameObject completionToast = GetPrivateField<GameObject>(
panel,
"completionToast");
Assert.That(completionToast.activeSelf, Is.True);
Assert.That(panel.transform.Find("Mission List Hint").gameObject.activeSelf,
Is.True,
"The left-side TAB hint should be visible in gameplay captures.");
RectTransform toastRect = completionToast.GetComponent<RectTransform>();
Assert.That(toastRect.anchorMin, Is.EqualTo(new Vector2(1f, 0f)));
Assert.That(toastRect.anchorMax, Is.EqualTo(new Vector2(1f, 0f)));
Assert.That(toastRect.pivot, Is.EqualTo(new Vector2(1f, 0f)));
CaptureCompletionToastBothResolutions(
canvas,
toastRect,
toastCaptureDirectory);
}
private Keyboard BeginVirtualKeyboardInput()
{
Assert.That(virtualKeyboardInputActive, Is.False);
previousBackgroundBehavior = InputSystem.settings.backgroundBehavior;
previousEditorInputBehavior =
InputSystem.settings.editorInputBehaviorInPlayMode;
previousRunInBackground = Application.runInBackground;
InputSystem.settings.backgroundBehavior =
InputSettings.BackgroundBehavior.IgnoreFocus;
InputSystem.settings.editorInputBehaviorInPlayMode =
InputSettings.EditorInputBehaviorInPlayMode
.AllDeviceInputAlwaysGoesToGameView;
Application.runInBackground = true;
virtualKeyboard = InputSystem.AddDevice<Keyboard>();
virtualKeyboard.MakeCurrent();
virtualKeyboardInputActive = true;
return virtualKeyboard;
}
private void EndVirtualKeyboardInput()
{
if (!virtualKeyboardInputActive)
{
return;
}
if (virtualKeyboard != null && virtualKeyboard.added)
{
InputSystem.RemoveDevice(virtualKeyboard);
}
InputSystem.settings.backgroundBehavior = previousBackgroundBehavior;
InputSystem.settings.editorInputBehaviorInPlayMode =
previousEditorInputBehavior;
Application.runInBackground = previousRunInBackground;
virtualKeyboard = null;
virtualKeyboardInputActive = false;
}
private static IEnumerator PressAndRelease(Keyboard keyboard, Key key)
{
InputSystem.QueueStateEvent(keyboard, new KeyboardState(key));
yield return null;
Assert.That(keyboard[key].isPressed, Is.True);
InputSystem.QueueStateEvent(keyboard, new KeyboardState());
yield return null;
Assert.That(keyboard[key].isPressed, Is.False);
}
private static IEnumerator PressEscapeAndAssertPauseIsSuppressedThisFrame(
Keyboard keyboard,
MissionAchievementPanel panel,
RunManager runManager)
{
InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.Escape));
yield return null;
Assert.That(keyboard.escapeKey.isPressed, Is.True);
Assert.That(panel.IsOpen, Is.False);
Assert.That(runManager.IsSelectionOpen, Is.False);
Assert.That(runManager.IsPaused, Is.False,
"Escape closes the list without opening pause.");
Assert.That(runManager.TryOpenPause(), Is.False,
"Pause is suppressed for the frame that closes the list.");
InputSystem.QueueStateEvent(keyboard, new KeyboardState());
yield return null;
Assert.That(keyboard.escapeKey.isPressed, Is.False);
}
private static void CaptureAtResolution(
Canvas canvas,
RectTransform panelRect,
RectTransform finalMissionRow,
int width,
int height,
string outputPath)
{
Camera worldCamera = Camera.main;
Assert.That(worldCamera, Is.Not.Null);
RenderTexture worldTarget = new(
width,
height,
24,
RenderTextureFormat.ARGB32);
RenderTexture uiTarget = new(
width,
height,
24,
RenderTextureFormat.ARGB32);
GameObject uiCameraObject = new("Mission Panel Capture Camera");
Camera uiCamera = uiCameraObject.AddComponent<Camera>();
Texture2D screenshot = null;
Texture2D uiScreenshot = null;
RenderTexture previousActive = RenderTexture.active;
RenderTexture previousWorldTarget = worldCamera.targetTexture;
float previousWorldAspect = worldCamera.aspect;
int previousWorldMask = worldCamera.cullingMask;
RenderTexture previousUiTarget = uiCamera.targetTexture;
float previousUiAspect = uiCamera.aspect;
RenderMode previousRenderMode = canvas.renderMode;
Camera previousCanvasCamera = canvas.worldCamera;
float previousCanvasPlaneDistance = canvas.planeDistance;
float previousCanvasScaleFactor = canvas.scaleFactor;
CanvasScaler scaler = canvas.GetComponent<CanvasScaler>();
bool previousScalerEnabled = scaler != null && scaler.enabled;
Transform[] canvasChildren = canvas.GetComponentsInChildren<Transform>(true);
int[] previousLayers = new int[canvasChildren.Length];
for (int i = 0; i < canvasChildren.Length; i++)
{
previousLayers[i] = canvasChildren[i].gameObject.layer;
}
try
{
worldTarget.Create();
uiTarget.Create();
worldCamera.targetTexture = worldTarget;
worldCamera.aspect = (float)width / height;
worldCamera.cullingMask &= ~(1 << 5);
uiCamera.enabled = false;
uiCamera.transform.SetPositionAndRotation(
worldCamera.transform.position,
worldCamera.transform.rotation);
uiCamera.orthographic = worldCamera.orthographic;
uiCamera.orthographicSize = worldCamera.orthographicSize;
uiCamera.aspect = (float)width / height;
uiCamera.clearFlags = CameraClearFlags.SolidColor;
uiCamera.backgroundColor = Color.clear;
uiCamera.cullingMask = 1 << 5;
uiCamera.targetTexture = uiTarget;
for (int i = 0; i < canvasChildren.Length; i++)
{
canvasChildren[i].gameObject.layer = 5;
}
canvas.renderMode = RenderMode.ScreenSpaceCamera;
canvas.worldCamera = uiCamera;
canvas.planeDistance = 1f;
if (scaler != null)
{
scaler.enabled = false;
}
canvas.scaleFactor = width / 1920f;
foreach (Graphic graphic in canvas.GetComponentsInChildren<Graphic>(true))
{
graphic.SetAllDirty();
}
Canvas.ForceUpdateCanvases();
AssertRectInsideRenderTarget(
panelRect,
canvas.GetComponent<RectTransform>(),
uiCamera,
width,
height);
AssertRectInsideRenderTarget(
finalMissionRow,
canvas.GetComponent<RectTransform>(),
uiCamera,
width,
height);
worldCamera.Render();
uiCamera.Render();
RenderTexture.active = worldTarget;
screenshot = new Texture2D(width, height, TextureFormat.RGB24, false);
screenshot.ReadPixels(new Rect(0f, 0f, width, height), 0, 0);
screenshot.Apply();
RenderTexture.active = uiTarget;
uiScreenshot = new Texture2D(width, height, TextureFormat.RGBA32, false);
uiScreenshot.ReadPixels(new Rect(0f, 0f, width, height), 0, 0);
uiScreenshot.Apply();
Color32[] worldPixels = screenshot.GetPixels32();
Color32[] uiPixels = uiScreenshot.GetPixels32();
for (int i = 0; i < worldPixels.Length; i++)
{
float alpha = uiPixels[i].a / 255f;
worldPixels[i] = new Color32(
(byte)Mathf.Min(255, uiPixels[i].r + worldPixels[i].r * (1f - alpha)),
(byte)Mathf.Min(255, uiPixels[i].g + worldPixels[i].g * (1f - alpha)),
(byte)Mathf.Min(255, uiPixels[i].b + worldPixels[i].b * (1f - alpha)),
255);
}
screenshot.SetPixels32(worldPixels);
screenshot.Apply();
byte[] imageBytes = screenshot.EncodeToPNG();
Assert.That(imageBytes.Length, Is.GreaterThan(5000),
"The captured Unity-rendered panel should contain visible UI content.");
File.WriteAllBytes(outputPath, imageBytes);
}
finally
{
RenderTexture.active = previousActive;
worldCamera.targetTexture = previousWorldTarget;
worldCamera.aspect = previousWorldAspect;
worldCamera.cullingMask = previousWorldMask;
uiCamera.targetTexture = previousUiTarget;
uiCamera.aspect = previousUiAspect;
for (int i = 0; i < canvasChildren.Length; i++)
{
if (canvasChildren[i] != null)
{
canvasChildren[i].gameObject.layer = previousLayers[i];
}
}
canvas.renderMode = previousRenderMode;
canvas.worldCamera = previousCanvasCamera;
canvas.planeDistance = previousCanvasPlaneDistance;
canvas.scaleFactor = previousCanvasScaleFactor;
if (scaler != null)
{
scaler.enabled = previousScalerEnabled;
}
Canvas.ForceUpdateCanvases();
if (screenshot != null)
{
Object.DestroyImmediate(screenshot);
}
if (uiScreenshot != null)
{
Object.DestroyImmediate(uiScreenshot);
}
Object.DestroyImmediate(uiCameraObject);
worldTarget.Release();
uiTarget.Release();
Object.DestroyImmediate(worldTarget);
Object.DestroyImmediate(uiTarget);
}
}
private static void CaptureBothResolutions(
Canvas canvas,
RectTransform panelRect,
RectTransform finalMissionRow,
string captureDirectory,
string viewName)
{
CaptureAtResolution(
canvas,
panelRect,
finalMissionRow,
1280,
720,
Path.Combine(captureDirectory, $"{viewName}-720p.png"));
CaptureAtResolution(
canvas,
panelRect,
finalMissionRow,
1920,
1080,
Path.Combine(captureDirectory, $"{viewName}-1080p.png"));
}
private static void CaptureCompletionToastBothResolutions(
Canvas canvas,
RectTransform toastRect,
string captureDirectory)
{
CaptureAtResolution(
canvas,
toastRect,
toastRect,
1280,
720,
Path.Combine(captureDirectory, "mission-toast-720p.png"));
CaptureAtResolution(
canvas,
toastRect,
toastRect,
1920,
1080,
Path.Combine(captureDirectory, "mission-toast-1080p.png"));
}
private static void AssertRectInsideRenderTarget(
RectTransform rect,
RectTransform canvas,
Camera camera,
int width,
int height)
{
Vector3[] corners = new Vector3[4];
rect.GetWorldCorners(corners);
for (int i = 0; i < corners.Length; i++)
{
Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(
camera,
corners[i]);
Assert.That(screenPoint.x, Is.GreaterThanOrEqualTo(-0.5f));
Assert.That(screenPoint.x, Is.LessThanOrEqualTo(width + 0.5f));
Assert.That(screenPoint.y, Is.GreaterThanOrEqualTo(-0.5f));
Assert.That(screenPoint.y, Is.LessThanOrEqualTo(height + 0.5f));
}
Assert.That(rect.rect.width, Is.LessThanOrEqualTo(canvas.rect.width));
Assert.That(rect.rect.height, Is.LessThanOrEqualTo(canvas.rect.height));
}
private static void SetPrivateField<T>(
object target,
string fieldName,
T value)
{
FieldInfo field = target.GetType().GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null, $"Missing field: {fieldName}");
field.SetValue(target, value);
}
private static T GetPrivateField<T>(object target, string fieldName)
{
FieldInfo field = target.GetType().GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null, $"Missing field: {fieldName}");
return (T)field.GetValue(target);
}
private static int GetQueuedCompletionNoticeCount(
MissionAchievementPanel panel)
{
object queue = GetPrivateField<object>(
panel,
"pendingCompletionNotices");
PropertyInfo countProperty = queue.GetType().GetProperty("Count");
Assert.That(countProperty, Is.Not.Null);
return (int)countProperty.GetValue(queue);
}
private static void CompleteMissionForToast(
RunMissionTracker tracker,
MissionAchievementPanel panel,
int missionIndex)
{
int[] progress = GetPrivateField<int[]>(tracker, "progress");
bool[] completed = GetPrivateField<bool[]>(tracker, "completed");
progress[missionIndex] = RunMissionTracker.GetDefinition(missionIndex).Target;
completed[missionIndex] = true;
InvokePrivateMethod(panel, "HandleProgressChanged");
}
private static void InvokePrivateMethod(object target, string methodName)
{
MethodInfo method = target.GetType().GetMethod(
methodName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(method, Is.Not.Null, $"Missing method: {methodName}");
method.Invoke(target, null);
}
private static IEnumerator LoadSceneAndWait()
{
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
while (!load.isDone)
{
yield return null;
}
yield return null;
yield return null;
}
}
}