Add mod documentation in seven languages
This commit is contained in:
1 parent
9f22af960c
commit
b06f8ce990
43 files changed
+1828
-20
No files matched your search
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Collection settings UI test",
|
||||
"sources": ["src/*.cs"]
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
namespace Mods.ModManager.SettingsTest
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Cthangover.Core.Bootstrap;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.UI.Menu;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Mods.Interface.Menu;
|
||||
using Godot;
|
||||
|
||||
/// <summary>Small-viewport scrolling, cancel, validation, automatic removal and persisted settings through real UI controls.</summary>
|
||||
public partial class CollectionSettingsTestDriver : Node
|
||||
{
|
||||
private const string FixtureId = "collection_settings_ui_test";
|
||||
private ModHubScreen hub;
|
||||
private ModCollectionsScreen editor;
|
||||
private CollectionSettingsDialog dialog;
|
||||
private string fixturePath;
|
||||
private string originalLocale;
|
||||
private int step;
|
||||
private int passed;
|
||||
private double elapsed;
|
||||
private double next;
|
||||
private bool done;
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (done)
|
||||
return;
|
||||
elapsed += delta;
|
||||
if (elapsed > 90)
|
||||
{
|
||||
Finish("timeout at step " + step);
|
||||
return;
|
||||
}
|
||||
if (elapsed < next)
|
||||
return;
|
||||
next = elapsed + 0.35;
|
||||
try { RunStep(); }
|
||||
catch (Exception ex) { Finish("step " + step + ": " + ex); }
|
||||
}
|
||||
|
||||
private void RunStep()
|
||||
{
|
||||
switch (step)
|
||||
{
|
||||
case 0:
|
||||
if (!Boot.IsReady)
|
||||
return;
|
||||
if (GetTree().CurrentScene?.FindChild("LauncherPlay", true, false) is Button launcher)
|
||||
{
|
||||
launcher.EmitSignal(BaseButton.SignalName.Pressed);
|
||||
return;
|
||||
}
|
||||
if (GetTree().CurrentScene?.FindChild("play", true, false) is not Button)
|
||||
return;
|
||||
originalLocale = TranslationServer.GetLocale();
|
||||
LocaleLoader.LoadLocale("ru");
|
||||
MakeFixture();
|
||||
var entry = MenuRegistry.GetButtonById("mods_hub");
|
||||
hub = MenuBuilder.OpenScene((Control)GetTree().CurrentScene, entry.ScenePath, "mods_hub") as ModHubScreen;
|
||||
Check(hub != null, "hub opens");
|
||||
hub.OpenCollectionEditor(FixtureId);
|
||||
editor = hub.FindChildren("*", "", true, false).OfType<ModCollectionsScreen>().Single();
|
||||
break;
|
||||
case 1:
|
||||
OpenForm();
|
||||
break;
|
||||
case 2:
|
||||
Check(dialog.FindChildren("*", "", true, false).OfType<CollectionSettingRow>().Count() == 5, "five fields read without loading game mods");
|
||||
Check(dialog.Title == "Параметры игры", "localized dialog title");
|
||||
Check(Row("battle.core").Value == "missing_core", "unavailable saved choice retained");
|
||||
dialog.Size = new Vector2I(640, 420);
|
||||
break;
|
||||
case 3:
|
||||
var scroll = dialog.GetNode<ScrollContainer>("FieldsScroll");
|
||||
Check(dialog.Size.Y <= 420, "dialog minimum does not exceed small viewport");
|
||||
Check(scroll.GetChild<Control>(0).Size.Y > scroll.Size.Y, "long form is contained in scroll viewport");
|
||||
Check(dialog.GetOkButton().GetGlobalRect().End.Y <= dialog.Size.Y, "Apply remains inside window");
|
||||
Check(dialog.GetCancelButton().GetGlobalRect().End.Y <= dialog.Size.Y, "Cancel remains inside window");
|
||||
scroll.ScrollVertical = 100000;
|
||||
break;
|
||||
case 4:
|
||||
Check(dialog.GetNode<ScrollContainer>("FieldsScroll").ScrollVertical > 0, "form scrolls to lower fields");
|
||||
Custom(Row("checks.dice"), "bad dice");
|
||||
dialog.EmitSignal(ConfirmationDialog.SignalName.Confirmed);
|
||||
Check(!dialog.IsQueuedForDeletion(), "invalid dice prevents Apply");
|
||||
Custom(Row("checks.dice"), "2d6");
|
||||
Custom(Row("stats.player"), "created_during_play");
|
||||
Select(Row("tile.renderer"), 0);
|
||||
dialog.EmitSignal(ConfirmationDialog.SignalName.Canceled);
|
||||
break;
|
||||
case 5:
|
||||
OpenForm();
|
||||
Check(Row("checks.dice").Value == "d20", "Cancel leaves original settings");
|
||||
Custom(Row("checks.dice"), "2d6");
|
||||
Custom(Row("stats.player"), "created_during_play");
|
||||
Select(Row("tile.renderer"), 0);
|
||||
dialog.EmitSignal(ConfirmationDialog.SignalName.Confirmed);
|
||||
break;
|
||||
case 6:
|
||||
editor.GetNode<Button>("Margin/Root/Header/Save").EmitSignal(BaseButton.SignalName.Pressed);
|
||||
break;
|
||||
case 7:
|
||||
var save = editor.FindChildren("*", "", true, false).OfType<AcceptDialog>().Single(d => d.Visible);
|
||||
save.EmitSignal(AcceptDialog.SignalName.Confirmed);
|
||||
break;
|
||||
case 8:
|
||||
var saved = ModCollectionStore.LoadUser().Single(c => c.Id == FixtureId);
|
||||
Check(saved.Settings["checks.dice"] == "2d6", "dice saved to collection package");
|
||||
Check(saved.Settings["stats.player"] == "created_during_play", "custom character saved");
|
||||
Check(saved.Settings["battle.core"] == "missing_core", "unavailable choice survives save");
|
||||
Check(saved.Settings["custom.fixture"] == "keep", "undeclared setting survives save");
|
||||
Check(!saved.Settings.ContainsKey("tile.renderer"), "Automatic deletes explicit key");
|
||||
editor.OpenCollection(FixtureId);
|
||||
OpenForm();
|
||||
break;
|
||||
case 9:
|
||||
Check(Row("checks.dice").Value == "2d6", "saved values restored on reopening");
|
||||
Check(Row("tile.renderer").Value == null, "automatic restored on reopening");
|
||||
dialog.EmitSignal(ConfirmationDialog.SignalName.Canceled);
|
||||
Finish(null);
|
||||
return;
|
||||
}
|
||||
step++;
|
||||
}
|
||||
|
||||
private void MakeFixture()
|
||||
{
|
||||
var path = Path.Combine(ModCollectionStore.UserPath, FixtureId);
|
||||
Check(!Directory.Exists(path), "fixture cannot overwrite player data");
|
||||
var source = ModCollectionStore.LoadBuiltIn().Single(c => c.Id == "rpg_plus");
|
||||
var clone = JsonSerializer.Deserialize<ModCollection>(JsonSerializer.Serialize(source));
|
||||
clone.Id = FixtureId;
|
||||
clone.Name = "Collection settings test";
|
||||
// A fixture must not inherit the game's localized display names.
|
||||
clone.Locale = null;
|
||||
clone.Settings["custom.fixture"] = "keep";
|
||||
clone.Settings["battle.core"] = "missing_core";
|
||||
Directory.CreateDirectory(path);
|
||||
fixturePath = path;
|
||||
File.WriteAllText(Path.Combine(path, "collection.json"), JsonSerializer.Serialize(clone));
|
||||
}
|
||||
|
||||
private void OpenForm()
|
||||
{
|
||||
editor.GetNode<Button>("Margin/Root/Tools/GameSettings").EmitSignal(BaseButton.SignalName.Pressed);
|
||||
dialog = editor.FindChildren("*", "", true, false).OfType<CollectionSettingsDialog>().Single(d => !d.IsQueuedForDeletion());
|
||||
}
|
||||
|
||||
private CollectionSettingRow Row(string key) => dialog.FindChildren("*", "", true, false).OfType<CollectionSettingRow>().Single(r => r.Key == key);
|
||||
|
||||
private static void Custom(CollectionSettingRow row, string value)
|
||||
{
|
||||
var select = row.FindChildren("*", "", true, false).OfType<OptionButton>().First();
|
||||
Select(row, select.ItemCount - 1);
|
||||
row.FindChildren("*", "", true, false).OfType<LineEdit>().First().Text = value;
|
||||
}
|
||||
|
||||
private static void Select(CollectionSettingRow row, int index)
|
||||
{
|
||||
var select = row.FindChildren("*", "", true, false).OfType<OptionButton>().First();
|
||||
select.Select(index);
|
||||
select.EmitSignal(OptionButton.SignalName.ItemSelected, (long)index);
|
||||
}
|
||||
|
||||
private void Check(bool condition, string label)
|
||||
{
|
||||
if (!condition)
|
||||
throw new InvalidOperationException(label);
|
||||
passed++;
|
||||
GameLogger.Log("TEST", "Collection settings: [OK] " + label);
|
||||
}
|
||||
|
||||
private void Finish(string failure)
|
||||
{
|
||||
done = true;
|
||||
hub?.QueueFree();
|
||||
CleanupFixture();
|
||||
if (originalLocale != null)
|
||||
LocaleLoader.LoadLocale(originalLocale);
|
||||
if (failure != null)
|
||||
GameLogger.Log("TEST", "Collection settings: " + failure, LogLevel.Error);
|
||||
GameLogger.Log("TEST", $"[TEST_RESULT] name=collection_settings passed={passed} failed={(failure == null ? 0 : 1)}");
|
||||
GetTree().Quit(failure == null ? 0 : 1);
|
||||
}
|
||||
|
||||
/// <summary>Also remove the owned fixture when the suite exits before Finish.</summary>
|
||||
public override void _ExitTree() => CleanupFixture();
|
||||
|
||||
private void CleanupFixture()
|
||||
{
|
||||
if (fixturePath != null && Directory.Exists(fixturePath))
|
||||
Directory.Delete(fixturePath, true);
|
||||
fixturePath = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Mods.ModManager.SettingsTest
|
||||
{
|
||||
using Cthangover.Core.Mods;
|
||||
|
||||
/// <summary>Explicit frontend-only UI test, leaving game assemblies unloaded.</summary>
|
||||
public class CollectionSettingsTestInitializer : IModInitializer
|
||||
{
|
||||
private static bool queued;
|
||||
public void OnModLoaded(string modId) { }
|
||||
public void OnModResourcesReady()
|
||||
{
|
||||
if (queued)
|
||||
return;
|
||||
queued = true;
|
||||
ModRoot.AddDeferred(new CollectionSettingsTestDriver());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "Mod documentation test",
|
||||
"depends": ["menu_base>=1"],
|
||||
"sources": ["src/*.cs"]
|
||||
}
|
||||
@@ -6,13 +6,19 @@ using Cthangover.Core.Localization;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.UI.Menu;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.BaseMenu;
|
||||
using Cthangover.Mods.Interface.Menu;
|
||||
using Cthangover.Mods.Interface.View;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.ModManager.DocumentationTest
|
||||
{
|
||||
/// <summary>Opens music_pack through the hub and exercises language selection on the actual page.</summary>
|
||||
/// <summary>
|
||||
/// Exercises the reference page, or reviews a real English page with
|
||||
/// --doc-mod=id. --doc-settings additionally captures the real Sound tab;
|
||||
/// the original settings file and language are restored even after failure.
|
||||
/// </summary>
|
||||
public partial class DocumentationTestDriver : Node
|
||||
{
|
||||
private double elapsed;
|
||||
@@ -24,6 +30,12 @@ namespace Mods.ModManager.DocumentationTest
|
||||
private ModHubScreen hub;
|
||||
private ModManagerScreen manager;
|
||||
private ModDetailView detail;
|
||||
private readonly string reviewMod = ReviewMod();
|
||||
private string reviewTitle;
|
||||
private SettingsMenu settings;
|
||||
private string settingsPath;
|
||||
private byte[] settingsBackup;
|
||||
private string settingsLanguage;
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
@@ -40,7 +52,10 @@ namespace Mods.ModManager.DocumentationTest
|
||||
stepAt = elapsed;
|
||||
try
|
||||
{
|
||||
RunStep();
|
||||
if (string.IsNullOrEmpty(reviewMod))
|
||||
RunStep();
|
||||
else
|
||||
RunReviewStep();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -82,7 +97,7 @@ namespace Mods.ModManager.DocumentationTest
|
||||
Check(Body().GetContentHeight() > 600, "long document lays out");
|
||||
var codeFont = Body().GetThemeFont("mono_font");
|
||||
Check(codeFont.HasChar('└') && codeFont.HasChar('─') && codeFont.HasChar('├'), "bundled monospace font covers tree glyphs");
|
||||
Check(ModDocumentation.Discover(ModRegistry.Instance.AllDiscoveredMods["music_pack"].FileProvider).Count == 2, "two bundled languages");
|
||||
Check(ModDocumentation.Discover(ModRegistry.Instance.AllDiscoveredMods["music_pack"].FileProvider).Count == LanguageCode.Supported.Length, "every project language is bundled");
|
||||
Shot("music_pack_ru");
|
||||
SelectLanguage("EN");
|
||||
break;
|
||||
@@ -100,16 +115,21 @@ namespace Mods.ModManager.DocumentationTest
|
||||
LocaleLoader.LoadLocale("ja");
|
||||
break;
|
||||
case 6:
|
||||
Check(Body().GetParsedText().Contains("a soundtrack for your game"), "missing Japanese falls back to English after locale change");
|
||||
Check(detail.GetNode<Label>("Scroll/Content/DocumentationBox/Source").Text.Contains("EN"), "actual fallback language is visible");
|
||||
manager.CloseDetail();
|
||||
manager.SetSearch("audio_base");
|
||||
Check(Body().GetParsedText().Contains("あなたのゲームのためのサウンドトラック"), "auto language follows the interface language (Japanese)");
|
||||
Check(detail.GetNode<Label>("Scroll/Content/DocumentationBox/Source").Text.Contains("JA"), "actual document language is visible");
|
||||
LocaleLoader.LoadLocale("de");
|
||||
break;
|
||||
case 7:
|
||||
var legacyCard = hub.FindChildren("*", "", true, false).OfType<ModCard>().First(IsVisibleCard);
|
||||
legacyCard.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
|
||||
Check(Body().GetParsedText().Contains("a soundtrack for your game"), "unbundled German falls back to English after locale change");
|
||||
LocaleLoader.LoadLocale("en");
|
||||
break;
|
||||
case 8:
|
||||
Check(detail.GetNode<Label>("Scroll/Content/DocumentationBox/Source").Text.Contains("EN"), "document language is visible for a bundled UI language");
|
||||
// A controlled legacy record stays valid as documentation
|
||||
// is gradually added to every real mod in the project.
|
||||
detail.Open(new ModCardData { Id = "documentation_legacy_fixture", Title = "Legacy fixture", Description = "Manifest-only description" });
|
||||
break;
|
||||
case 9:
|
||||
Check(!detail.GetNode<Control>("Scroll/Content/DocumentationBox").Visible, "legacy mod keeps manifest description");
|
||||
Check(!string.IsNullOrWhiteSpace(detail.GetNode<Label>("Scroll/Content/DescriptionBox/Text").Text), "legacy description remains readable");
|
||||
Finish(null);
|
||||
@@ -118,9 +138,95 @@ namespace Mods.ModManager.DocumentationTest
|
||||
step++;
|
||||
}
|
||||
|
||||
private static string ReviewMod()
|
||||
{
|
||||
foreach (var arg in OS.GetCmdlineUserArgs())
|
||||
if (arg.StartsWith("--doc-mod=", StringComparison.Ordinal))
|
||||
return arg.Substring("--doc-mod=".Length);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void RunReviewStep()
|
||||
{
|
||||
switch (step)
|
||||
{
|
||||
case 0:
|
||||
if (!Boot.IsReady)
|
||||
return;
|
||||
if (GetTree().CurrentScene?.FindChild("LauncherPlay", true, false) is Button launcherPlay)
|
||||
{
|
||||
launcherPlay.EmitSignal(BaseButton.SignalName.Pressed);
|
||||
return;
|
||||
}
|
||||
if (GetTree().CurrentScene?.FindChild("play", true, false) is not Button)
|
||||
return;
|
||||
originalLocale = TranslationServer.GetLocale();
|
||||
settingsLanguage = GameData.Instance.Settings.Language;
|
||||
settingsPath = ProjectSettings.GlobalizePath("user://settings.json");
|
||||
settingsBackup = File.Exists(settingsPath) ? File.ReadAllBytes(settingsPath) : null;
|
||||
settings = GetTree().CurrentScene.GetNode<SettingsMenu>("SettingsPanel");
|
||||
settings.Open();
|
||||
var language = settings.GetNode<OptionButton>("Window/Column/Body/Pages/General/LangRow/LangSelect");
|
||||
var english = Array.IndexOf(LanguageCode.Supported, "en");
|
||||
language.Select(english);
|
||||
language.EmitSignal(OptionButton.SignalName.ItemSelected, (long)english);
|
||||
settings.ShowTab(1);
|
||||
break;
|
||||
case 1:
|
||||
Check(TranslationServer.GetLocale().StartsWith("en", StringComparison.Ordinal), "English UI selected through Settings");
|
||||
if (OS.GetCmdlineUserArgs().Contains("--doc-settings") || OS.GetCmdlineUserArgs().Contains("--doc-settings-only"))
|
||||
{
|
||||
var audio = settings.GetNode<Control>("Window/Column/Body/Pages/Audio");
|
||||
Check(audio.IsVisibleInTree(), "actual Sound tab visible");
|
||||
Shot(reviewMod + "_sound_settings");
|
||||
}
|
||||
if (OS.GetCmdlineUserArgs().Contains("--doc-settings-only"))
|
||||
{
|
||||
Finish(null);
|
||||
return;
|
||||
}
|
||||
settings.Visible = false;
|
||||
var entry = MenuRegistry.GetButtonById("mods_hub");
|
||||
hub = MenuBuilder.OpenScene((Control)GetTree().CurrentScene, entry.ScenePath, "mods_hub") as ModHubScreen;
|
||||
Check(hub != null, "hub opens for documentation review");
|
||||
break;
|
||||
case 2:
|
||||
manager = hub.FindChildren("*", "", true, false).OfType<ModManagerScreen>().First();
|
||||
manager.SetSearch(reviewMod);
|
||||
break;
|
||||
case 3:
|
||||
// The search also matches test suites such as mod_manager__documentation; open the reviewed mod itself.
|
||||
reviewTitle = ModRegistry.Instance.AllDiscoveredMods[reviewMod].DisplayTitle ?? reviewMod;
|
||||
var card = hub.FindChildren("*", "", true, false).OfType<ModCard>().First(IsReviewedCard);
|
||||
card.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
|
||||
break;
|
||||
case 4:
|
||||
detail = hub.FindChildren("*", "", true, false).OfType<ModDetailView>().First(IsVisibleDetail);
|
||||
var provider = ModRegistry.Instance.AllDiscoveredMods[reviewMod].FileProvider;
|
||||
var document = ModDocumentation.Select(ModDocumentation.Discover(provider), "en");
|
||||
Check(document?.Path == "docs/en.md", "English entry point exists for " + reviewMod);
|
||||
var heading = document.Text.Split('\n')[0].TrimStart('#', ' ', '\uFEFF').TrimEnd('\r');
|
||||
var shown = Body().GetParsedText();
|
||||
Check(shown.Contains(heading), "actual page displays the selected English document; heading '" + heading + "', shown '" + shown.Substring(0, Math.Min(160, shown.Length)) + "'");
|
||||
Check(!Body().GetParsedText().Contains(TranslationServer.Translate("mod_manager/docs_image_unavailable").ToString()), "all page images resolve");
|
||||
Check(Body().GetContentHeight() > 300, "page lays out for scrolling");
|
||||
Shot(reviewMod + "_documentation_en");
|
||||
LocaleLoader.LoadLocale("ru");
|
||||
break;
|
||||
case 5:
|
||||
Check(detail.GetNode<Label>("Scroll/Content/DocumentationBox/Source").Text.Contains("EN"), "actual English fallback is identified");
|
||||
Check(!string.IsNullOrWhiteSpace(Body().GetParsedText()), "fallback document remains readable");
|
||||
Shot(reviewMod + "_documentation_fallback");
|
||||
Finish(null);
|
||||
break;
|
||||
}
|
||||
step++;
|
||||
}
|
||||
|
||||
private ModMarkdownView Body() => detail.GetNode<ModMarkdownView>("Scroll/Content/DocumentationBox/Body");
|
||||
private OptionButton Languages() => detail.GetNode<OptionButton>("Scroll/Content/DocumentationBox/Header/Languages");
|
||||
private static bool IsVisibleCard(ModCard card) => card.IsVisibleInTree();
|
||||
private bool IsReviewedCard(ModCard card) => IsVisibleCard(card) && card.GetNode<Label>("VBox/Body/Box/Title").Text == reviewTitle;
|
||||
private static bool IsVisibleDetail(ModDetailView page) => page.IsVisibleInTree();
|
||||
private static bool IsScreenshotArg(string arg) => arg.StartsWith("--shots=");
|
||||
|
||||
@@ -175,6 +281,14 @@ namespace Mods.ModManager.DocumentationTest
|
||||
return;
|
||||
done = true;
|
||||
hub?.QueueFree();
|
||||
if (settingsPath != null)
|
||||
{
|
||||
GameData.Instance.Settings.Language = settingsLanguage;
|
||||
if (settingsBackup == null)
|
||||
File.Delete(settingsPath);
|
||||
else
|
||||
File.WriteAllBytes(settingsPath, settingsBackup);
|
||||
}
|
||||
if (originalLocale != null)
|
||||
LocaleLoader.LoadLocale(originalLocale);
|
||||
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
|
||||
|
||||
Reference in new issue
Block a user