Danro-Jump/Assets/Tests/EditMode/Editor/ServiceTextInputModalWindowTests.cs
Danil Iarosh c1b72854f4 Implement prize QR and service UI polish
Подробные изменения:

- Добавлена система призовых уровней и выдачи призов с QR-контуром Telegram, шаблоном сообщения и генерацией QR без внешних библиотек.

- Доработаны сервисные настройки: действия кнопок, модальные окна, ввод с джойстика/клавиатуры, статистика, настройки призов и маршрутизация QR.

- Переработаны gameplay HUD и overlay-панели на UI Toolkit со спрайтовыми плашками, DEV BUILD-статистикой, экраном продолжения и экраном результата приза.

- Исправлены сценарии смерти, GameOver, продолжения игры, начисления очков за высоту и перехода в главное меню.

- Перенесены UI-шаблоны из Resources в Assets/UI, подключена Addressables-загрузка шаблонов настроек и standalone-стили для MainMenuModals.

- Добавлены EditMode/PlayMode тесты для призов, QR, arcade input, UI-модалок, scene flow, HUD и gameplay-сессии.

- Обновлены сцены, настройки Addressables, service settings database, документация и дорожная карта.

Проверки:

- Targeted EditMode QR/UI/settings: 23/23 passed

- Targeted UI templates/EditMode: 17/17 passed

- MainMenuUxmlTests: 3/3 passed

- Full EditMode: 129/129 passed

- PlayMode: 6/6 passed

- Runtime Addressables template load check: loaded=5

- Unity Console после финального refresh: 0 errors
2026-06-12 16:43:28 +03:00

333 lines
12 KiB
C#

using DanroJump.Settings;
using DanroJump.UI.ArcadeInput;
using DanroJump.UI.MainMenu;
using NUnit.Framework;
using System.Reflection;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanroJump.Tests.EditMode
{
public sealed class ServiceTextInputModalWindowTests
{
[Test]
public void SetContext_BuildsSlotsAndPreview()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
modal.SetContext(definition, "+799", ArcadeStringInputProfile.Phone(maxLength: 5), _ => { });
Assert.That(root.Q<Label>("ServiceTextInputSetting").text, Is.EqualTo("payment.supportNumber"));
Assert.That(root.Q<Label>("ServiceTextInputPreview").text, Is.EqualTo("+799"));
Assert.That(root.Q<VisualElement>("ServiceTextInputSlots").childCount, Is.EqualTo(5));
modal.Dispose();
}
[Test]
public void Confirm_InvokesCallbackOnlyOnce()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
var calls = 0;
var confirmedValue = "";
modal.SetContext(
definition,
"+799",
ArcadeStringInputProfile.Phone(maxLength: 5),
value =>
{
calls++;
confirmedValue = value;
});
Assert.That(modal.Confirm(), Is.True);
Assert.That(modal.Confirm(), Is.False);
Assert.That(calls, Is.EqualTo(1));
Assert.That(confirmedValue, Is.EqualTo("+799"));
modal.Dispose();
}
[Test]
public void DeleteCurrentCharacter_ClearsSelectedSlotBeforeConfirm()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
var confirmedValue = "";
modal.SetContext(
definition,
"79",
ArcadeStringInputProfile.Phone(maxLength: 3),
value => confirmedValue = value);
modal.HandleNavigationKey(root, KeyCode.RightArrow);
modal.DeleteCurrentCharacter();
modal.Confirm();
Assert.That(confirmedValue, Is.EqualTo("7"));
modal.Dispose();
}
[Test]
public void KeyboardSubmit_MovesAcrossCharactersAndConfirmsOnAction()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
var confirmedValue = "";
modal.SetContext(
definition,
"79",
ArcadeStringInputProfile.Phone(maxLength: 2),
value => confirmedValue = value);
modal.HandleNavigationKey(root, KeyCode.Return);
modal.HandleNavigationKey(root, KeyCode.Return);
modal.HandleNavigationKey(root, KeyCode.Return);
Assert.That(confirmedValue, Is.EqualTo("79"));
modal.Dispose();
}
[Test]
public void NavigationSubmit_DoesNotMoveTwiceInSameFrame()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
modal.SetContext(
definition,
"+79",
ArcadeStringInputProfile.Phone(maxLength: 3),
_ => { });
InvokeNavigationSubmit(modal);
InvokeNavigationSubmit(modal);
Assert.That(GetActiveSlotText(root), Is.EqualTo("7"));
modal.Dispose();
}
[Test]
public void NavigationSubmit_DoesNotMoveTwiceInsideDebounceWindow()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
modal.SetContext(
definition,
"+79",
ArcadeStringInputProfile.Phone(maxLength: 3),
_ => { });
InvokeNavigationSubmit(modal);
SetPrivateField(modal, "lastNavigationSubmitFrame", -1);
SetPrivateField(modal, "lastNavigationSubmitTime", Time.unscaledTime);
InvokeNavigationSubmit(modal);
Assert.That(GetActiveSlotText(root), Is.EqualTo("7"));
modal.Dispose();
}
[Test]
public void NavigationMoveRight_DoesNotSkipSlotInSameFrame()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
modal.SetContext(
definition,
"+79",
ArcadeStringInputProfile.Phone(maxLength: 3),
_ => { });
modal.HandleNavigationMove(root, NavigationMoveEvent.Direction.Right);
modal.HandleNavigationMove(root, NavigationMoveEvent.Direction.Right);
Assert.That(GetActiveSlotText(root), Is.EqualTo("7"));
modal.Dispose();
}
[Test]
public void NavigationMoveRight_DoesNotSkipSlotInsideDebounceWindow()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
modal.SetContext(
definition,
"+79",
ArcadeStringInputProfile.Phone(maxLength: 3),
_ => { });
modal.HandleNavigationMove(root, NavigationMoveEvent.Direction.Right);
SetPrivateField(modal, "lastDirectionalStepFrame", -1);
SetPrivateField(modal, "lastDirectionalStepBlockedUntil", Time.unscaledTime + 1f);
modal.HandleNavigationMove(root, NavigationMoveEvent.Direction.Right);
Assert.That(GetActiveSlotText(root), Is.EqualTo("7"));
modal.Dispose();
}
[Test]
public void KeyboardMoveRight_DoesNotSkipSlotInsideDirectionalGate()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
modal.SetContext(
definition,
"+79",
ArcadeStringInputProfile.Phone(maxLength: 3),
_ => { });
modal.HandleNavigationKey(root, KeyCode.RightArrow);
modal.HandleNavigationKey(root, KeyCode.RightArrow);
Assert.That(GetActiveSlotText(root), Is.EqualTo("7"));
modal.Dispose();
}
[Test]
public void KeyboardMoveRight_CanRepeatAfterDirectionalGateExpires()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
modal.SetContext(
definition,
"+79",
ArcadeStringInputProfile.Phone(maxLength: 3),
_ => { });
modal.HandleNavigationKey(root, KeyCode.RightArrow);
SetPrivateField(modal, "lastDirectionalStepFrame", -1);
SetPrivateField(modal, "lastDirectionalStepBlockedUntil", Time.unscaledTime - 1f);
modal.HandleNavigationKey(root, KeyCode.RightArrow);
Assert.That(GetActiveSlotText(root), Is.EqualTo("9"));
modal.Dispose();
}
[Test]
public void KeyboardAndNavigationMoveRight_DoNotCombineIntoDoubleStep()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
modal.SetContext(
definition,
"+79",
ArcadeStringInputProfile.Phone(maxLength: 3),
_ => { });
modal.HandleNavigationKey(root, KeyCode.RightArrow);
SetPrivateField(modal, "lastDirectionalStepFrame", -1);
SetPrivateField(modal, "lastDirectionalStepBlockedUntil", Time.unscaledTime + 1f);
modal.HandleNavigationMove(root, NavigationMoveEvent.Direction.Right);
Assert.That(GetActiveSlotText(root), Is.EqualTo("7"));
modal.Dispose();
}
[Test]
public void NavigationMove_AllowsDifferentDirectionInsideDebounceWindow()
{
var root = CreateRoot();
var modal = new ServiceTextInputModalWindow(root);
var definition = CreateStringDefinition("payment.supportNumber");
modal.SetContext(
definition,
"+79",
ArcadeStringInputProfile.Phone(maxLength: 3),
_ => { });
modal.HandleNavigationMove(root, NavigationMoveEvent.Direction.Right);
SetPrivateField(modal, "lastDirectionalStepFrame", -1);
SetPrivateField(modal, "lastDirectionalStepBlockedUntil", Time.unscaledTime + 1f);
modal.HandleNavigationMove(root, NavigationMoveEvent.Direction.Left);
Assert.That(GetActiveSlotText(root), Is.EqualTo("+"));
modal.Dispose();
}
private static ServiceSettingDefinition CreateStringDefinition(string key)
{
return TestObjectFactory.CreateSettingDefinition(
key,
ServiceSettingValueType.String,
ServiceSettingValue.String(""));
}
private static void InvokeNavigationSubmit(ServiceTextInputModalWindow modal)
{
using var evt = NavigationSubmitEvent.GetPooled();
typeof(ServiceTextInputModalWindow)
.GetMethod("HandleNavigationSubmit", BindingFlags.Instance | BindingFlags.NonPublic)
.Invoke(modal, new object[] { evt });
}
private static void SetPrivateField(object target, string fieldName, object value)
{
typeof(ServiceTextInputModalWindow)
.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(target, value);
}
private static string GetActiveSlotText(VisualElement root)
{
var slots = root.Q<VisualElement>("ServiceTextInputSlots");
foreach (var child in slots.Children())
{
if (child is Label label && label.ClassListContains("service-text-input-slot--active"))
{
return label.text;
}
}
return "";
}
private static VisualElement CreateRoot()
{
var root = new VisualElement();
root.Add(new Label { name = "ServiceTextInputTitle" });
root.Add(new Label { name = "ServiceTextInputSetting" });
root.Add(new Label { name = "ServiceTextInputPreview" });
root.Add(new VisualElement { name = "ServiceTextInputSlots" });
root.Add(new Label { name = "ServiceTextInputSymbol" });
root.Add(new Label { name = "ServiceTextInputStatus" });
root.Add(new Button { name = "ServiceTextInputConfirmButton" });
root.Add(new Button { name = "ServiceTextInputDeleteButton" });
root.Add(new Button { name = "ServiceTextInputCancelButton" });
return root;
}
}
}