help messages
This commit is contained in:
1 parent
dd465cc093
commit
b6c61ea3df
20 files changed
+659
-5
No files matched your search
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Mod Manager Help Test",
|
||||
"description": "Integration test of the mod manager's help: opens the Mods & Collections hub on every level (mods grid, a mod's page, games gallery, scheme editor with a selected mechanic and mod, help panels) and checks that every visible button, toggle, list and input explains itself with a tooltip. Activated only by --itest=mod_help (with --frontend); --shots=<dir> saves every level.",
|
||||
"sources": [
|
||||
"src/*.cs"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Bootstrap;
|
||||
using Cthangover.Core.UI.Menu;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Mods.Interface.Menu;
|
||||
using Cthangover.Mods.Interface.View;
|
||||
using Godot;
|
||||
using Mods.ModManager;
|
||||
|
||||
namespace Mods.ModManagerHelpTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Walks the Mods & Collections hub like a player who hovers over everything: the mods grid, a mod's
|
||||
/// page, the games gallery, the scheme editor (with a mechanic and then a mod selected) and the help
|
||||
/// panel of each level. On every level each visible button, toggle, list and input must carry a tooltip,
|
||||
/// and no tooltip may sit on a control that ignores the mouse (Godot never shows it there). One step
|
||||
/// per tick, so deferred layout settles. <c>--shots=<dir></c> saves every level.
|
||||
/// Prints <c>[TEST_RESULT] name=mod_help</c>.
|
||||
/// </summary>
|
||||
public partial class ModHelpTestDriverNode : Node
|
||||
{
|
||||
private const string HubId = "mods_hub";
|
||||
private const string GameId = "novel_template";
|
||||
private const float TimeoutSeconds = 90f;
|
||||
|
||||
private readonly List<string> failures = new();
|
||||
private int passed;
|
||||
private int step;
|
||||
private double elapsed;
|
||||
private double stepAt;
|
||||
private bool done;
|
||||
private ModHubScreen hub;
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (done)
|
||||
return;
|
||||
|
||||
elapsed += delta;
|
||||
if (elapsed > TimeoutSeconds)
|
||||
{
|
||||
failures.Add($"timeout at step {step}");
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (elapsed - stepAt < 0.5)
|
||||
return;
|
||||
stepAt = elapsed;
|
||||
|
||||
try
|
||||
{
|
||||
RunStep();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failures.Add($"step {step}: {ex.GetType().Name}: {ex.Message}");
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void RunStep()
|
||||
{
|
||||
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;
|
||||
var entry = MenuRegistry.GetButtonById(HubId);
|
||||
hub = MenuBuilder.OpenScene((Control)GetTree().CurrentScene, entry?.ScenePath, HubId) as ModHubScreen;
|
||||
Check("the hub opens", hub != null);
|
||||
if (hub == null)
|
||||
Finish();
|
||||
break;
|
||||
|
||||
case 1:
|
||||
PressTab("mod_manager/hub_tab_mods");
|
||||
break;
|
||||
|
||||
case 2:
|
||||
CheckLevel("mods");
|
||||
var card = hub.FindChildren("*", "", true, false).OfType<ModCard>().FirstOrDefault(c => c.IsVisibleInTree());
|
||||
Check("the grid shows mod cards", card != null);
|
||||
card?.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
|
||||
break;
|
||||
|
||||
case 3:
|
||||
CheckLevel("mod_page");
|
||||
PressTab("mod_manager/hub_tab_collections");
|
||||
break;
|
||||
|
||||
case 4:
|
||||
CheckLevel("gallery");
|
||||
hub.OpenCollectionEditor(GameId);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
{
|
||||
CheckLevel("scheme");
|
||||
var group = hub.FindChildren("*", "", true, false).OfType<CollectionGroupCard>().FirstOrDefault(g => g.IsVisibleInTree());
|
||||
Check("the scheme shows mechanics", group != null);
|
||||
Click(group);
|
||||
break;
|
||||
}
|
||||
|
||||
case 6:
|
||||
{
|
||||
CheckLevel("scheme_mechanic");
|
||||
var chip = hub.FindChildren("*", "", true, false).OfType<CollectionModChip>().FirstOrDefault(c => c.IsVisibleInTree());
|
||||
Check("the mechanic shows its mods", chip != null);
|
||||
Click(chip);
|
||||
break;
|
||||
}
|
||||
|
||||
case 7:
|
||||
CheckLevel("scheme_mod");
|
||||
PressHelp();
|
||||
break;
|
||||
|
||||
case 8:
|
||||
CheckHelp("help_scheme");
|
||||
PressTab("mod_manager/hub_tab_collections");
|
||||
break;
|
||||
|
||||
case 9:
|
||||
PressHelp();
|
||||
break;
|
||||
|
||||
case 10:
|
||||
CheckHelp("help_collections");
|
||||
PressTab("mod_manager/hub_tab_mods");
|
||||
break;
|
||||
|
||||
case 11:
|
||||
PressHelp();
|
||||
break;
|
||||
|
||||
case 12:
|
||||
CheckHelp("help_mods");
|
||||
hub.QueueFree();
|
||||
Finish();
|
||||
break;
|
||||
}
|
||||
|
||||
step++;
|
||||
}
|
||||
|
||||
/// <summary>A left click on the frame of a scheme mechanic or mod chip (their first panel takes the mouse).</summary>
|
||||
private static void Click(Control owner)
|
||||
{
|
||||
var panel = owner?.GetChildren().OfType<PanelContainer>().FirstOrDefault();
|
||||
panel?.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
|
||||
panel?.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = false });
|
||||
}
|
||||
|
||||
private void PressTab(string key)
|
||||
{
|
||||
var text = TranslationServer.Translate(key).ToString();
|
||||
var tab = Buttons(hub).FirstOrDefault(b => TextOf(b) == text && b.IsVisibleInTree() && b.ToggleMode);
|
||||
tab ??= Buttons(hub).FirstOrDefault(b => TextOf(b) == text && b.IsVisibleInTree());
|
||||
Check($"tab '{text}' shown", tab != null);
|
||||
tab?.EmitSignal(BaseButton.SignalName.Pressed);
|
||||
}
|
||||
|
||||
private void PressHelp()
|
||||
{
|
||||
var help = Buttons(hub).FirstOrDefault(b => b.Name == "HelpButton" && b.IsVisibleInTree());
|
||||
Check("the header offers help", help != null);
|
||||
help?.EmitSignal(BaseButton.SignalName.Pressed);
|
||||
}
|
||||
|
||||
private void CheckHelp(string level)
|
||||
{
|
||||
var panel = hub.FindChildren("*", "", true, false).OfType<ModHelpPanel>().FirstOrDefault(p => p.IsVisibleInTree());
|
||||
Check($"{level}: the help panel opens", panel != null);
|
||||
var body = panel?.FindChildren("*", "RichTextLabel", true, false).OfType<RichTextLabel>().FirstOrDefault();
|
||||
Check($"{level}: the help explains something", body != null && body.GetParsedText().Length > 200);
|
||||
Shot(level);
|
||||
CheckLevel(level);
|
||||
panel?.Close();
|
||||
}
|
||||
|
||||
/// <summary>Every visible control the player can press, pick or type into explains itself on hover.</summary>
|
||||
private void CheckLevel(string level)
|
||||
{
|
||||
Shot(level);
|
||||
var silent = new List<string>();
|
||||
var deaf = new List<string>();
|
||||
foreach (var control in hub.FindChildren("*", "", true, false).OfType<Control>())
|
||||
{
|
||||
if (!control.IsVisibleInTree())
|
||||
continue;
|
||||
var tip = control.TooltipText;
|
||||
if (Interactive(control) && string.IsNullOrWhiteSpace(tip))
|
||||
silent.Add(Describe(control));
|
||||
if (!string.IsNullOrWhiteSpace(tip) && control.MouseFilter == Control.MouseFilterEnum.Ignore)
|
||||
deaf.Add(Describe(control));
|
||||
}
|
||||
|
||||
Check($"{level}: every control has a tooltip" + (silent.Count > 0 ? ": missing on " + string.Join(", ", silent) : string.Empty), silent.Count == 0);
|
||||
Check($"{level}: no tooltip on a control that ignores the mouse" + (deaf.Count > 0 ? ": " + string.Join(", ", deaf) : string.Empty), deaf.Count == 0);
|
||||
}
|
||||
|
||||
private static bool Interactive(Control control)
|
||||
=> (control is BaseButton || control is LineEdit || control is TextEdit)
|
||||
&& control.MouseFilter != Control.MouseFilterEnum.Ignore
|
||||
&& !InsidePopup(control);
|
||||
|
||||
/// <summary>Items inside a popup menu follow the menu itself, not their own tooltips.</summary>
|
||||
private static bool InsidePopup(Control control)
|
||||
{
|
||||
for (Node parent = control.GetParent(); parent != null; parent = parent.GetParent())
|
||||
{
|
||||
if (parent is PopupMenu)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string Describe(Control control)
|
||||
{
|
||||
var text = control switch
|
||||
{
|
||||
Button button => button.Text,
|
||||
LineEdit edit => edit.PlaceholderText,
|
||||
_ => string.Empty,
|
||||
};
|
||||
return $"{control.GetClass()} '{control.Name}'" + (string.IsNullOrEmpty(text) ? string.Empty : $" \"{text}\"");
|
||||
}
|
||||
|
||||
private static IEnumerable<BaseButton> Buttons(Node root)
|
||||
=> root == null ? Enumerable.Empty<BaseButton>() : root.FindChildren("*", "", true, false).OfType<BaseButton>();
|
||||
|
||||
private static string TextOf(BaseButton button)
|
||||
=> button is Button clickable ? clickable.Text : string.Empty;
|
||||
|
||||
/// <summary>With <c>--shots=<dir></c> saves the window as <paramref name="name"/>.png (windowed runs only).</summary>
|
||||
private void Shot(string name)
|
||||
{
|
||||
string dir = null;
|
||||
foreach (var arg in OS.GetCmdlineUserArgs())
|
||||
{
|
||||
if (arg.StartsWith("--shots="))
|
||||
dir = arg.Substring("--shots=".Length);
|
||||
}
|
||||
if (string.IsNullOrEmpty(dir) || DisplayServer.GetName() == "headless")
|
||||
return;
|
||||
dir = ProjectSettings.GlobalizePath(dir);
|
||||
Directory.CreateDirectory(dir);
|
||||
ViewportCapture.SavePng(GetViewport(), Path.Combine(dir, name + ".png"));
|
||||
}
|
||||
|
||||
private void Check(string name, bool ok)
|
||||
{
|
||||
if (ok)
|
||||
{
|
||||
passed++;
|
||||
GameLogger.Log("TEST", $"ModHelpTest: [OK] {name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
failures.Add(name);
|
||||
GameLogger.Log("TEST", $"ModHelpTest: [FAIL] {name}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void Finish()
|
||||
{
|
||||
if (done)
|
||||
return;
|
||||
done = true;
|
||||
GameLogger.Log("TEST", $"[TEST_RESULT] name=mod_help passed={passed} failed={failures.Count}");
|
||||
foreach (var failure in failures)
|
||||
GameLogger.Log("TEST", $"ModHelpTest: failure: {failure}", LogLevel.Error);
|
||||
GetTree().Quit(failures.Count == 0 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.ModManagerHelpTest
|
||||
{
|
||||
/// <summary>Queues <see cref="ModHelpTestDriverNode"/> only for <c>--itest=mod_help</c>.</summary>
|
||||
public class ModHelpTestInitializer : IModInitializer
|
||||
{
|
||||
private static bool queued;
|
||||
|
||||
public void OnModLoaded(string modId) { }
|
||||
|
||||
public void OnModResourcesReady()
|
||||
{
|
||||
if (queued || !OS.GetCmdlineUserArgs().Contains("--itest=mod_help"))
|
||||
return;
|
||||
|
||||
queued = true;
|
||||
GameLogger.Log("TEST", "ModHelpTest: activating");
|
||||
ModRoot.AddDeferred(new ModHelpTestDriverNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user