Skip to content

Commit 3d4e791

Browse files
fix(core): address code review feedback
- Add BootstrapText.SafeFormat helper with try/catch fallback for malformed user-edited format strings - Replace all string.Format calls with SafeFormat in Bootstrap.cs - Use nameof(BootstrapText.*) instead of string literals in editor UI - Use BindProperty for JTextField binding (proper undo/prefab support) - Add tests for SafeFormat, Default field validation, exact field count, sub-headers, and reset logic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net>
1 parent ddc7645 commit 3d4e791

File tree

4 files changed

+189
-49
lines changed

4 files changed

+189
-49
lines changed

UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ private async void Initialize()
210210
}
211211
catch (Exception e)
212212
{
213-
await Prompt.ShowDialogAsync(text.dialogTitleError, string.Format(text.dialogInitFailed, e.Message), text.buttonOk, null);
213+
await Prompt.ShowDialogAsync(text.dialogTitleError, BootstrapText.SafeFormat(text.dialogInitFailed, e.Message), text.buttonOk, null);
214214
Application.Quit();
215215
}
216216
}
@@ -244,13 +244,13 @@ private async UniTask InitializeGame()
244244
OnVersionUpdate = version => versionText.text = $"v{Application.version}.{version}",
245245
OnDownloadPrompt = async (count, size) =>
246246
await Prompt.ShowDialogAsync(t.dialogTitleNotice,
247-
string.Format(t.dialogDownloadPrompt, count, $"{size / 1024f / 1024f:F2}"),
247+
BootstrapText.SafeFormat(t.dialogDownloadPrompt, count, $"{size / 1024f / 1024f:F2}"),
248248
t.buttonDownload, t.buttonCancel),
249249
OnDownloadProgress = data =>
250250
{
251251
if (updateStatusText != null)
252252
{
253-
updateStatusText.text = string.Format(t.dialogDownloadProgress,
253+
updateStatusText.text = BootstrapText.SafeFormat(t.dialogDownloadProgress,
254254
data.CurrentDownloadCount, data.TotalDownloadCount,
255255
$"{data.CurrentDownloadBytes / 1024f / 1024f:F2}",
256256
$"{data.TotalDownloadBytes / 1024f / 1024f:F2}");
@@ -339,7 +339,7 @@ await Prompt.ShowDialogAsync(t.dialogTitleNotice,
339339
OnError = async exception =>
340340
{
341341
await Prompt.ShowDialogAsync(t.dialogTitleError,
342-
string.Format(t.dialogSceneLoadFailed, exception.Message),
342+
BootstrapText.SafeFormat(t.dialogSceneLoadFailed, exception.Message),
343343
t.buttonRetry, null);
344344
}
345345
};
@@ -353,7 +353,7 @@ await Prompt.ShowDialogAsync(t.dialogTitleError,
353353
catch (Exception ex)
354354
{
355355
Debug.LogError($"Initialization failed with exception: {ex}");
356-
await Prompt.ShowDialogAsync(text.dialogTitleError, string.Format(text.dialogInitException, ex.Message), text.buttonOk, text.buttonCancel);
356+
await Prompt.ShowDialogAsync(text.dialogTitleError, BootstrapText.SafeFormat(text.dialogInitException, ex.Message), text.buttonOk, text.buttonCancel);
357357
// Continue the loop to retry
358358
}
359359
}
@@ -404,7 +404,7 @@ private async UniTask LoadHotCode(Assembly hotUpdateAss)
404404
catch (Exception e)
405405
{
406406
Debug.LogError($"Failed to invoke hot update method {hotUpdateMethodName}: {e}");
407-
await Prompt.ShowDialogAsync(text.dialogTitleError, string.Format(text.dialogFunctionCallFailed, e.Message), text.buttonExit, null);
407+
await Prompt.ShowDialogAsync(text.dialogTitleError, BootstrapText.SafeFormat(text.dialogFunctionCallFailed, e.Message), text.buttonExit, null);
408408
Application.Quit();
409409
}
410410
}

UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,21 @@ public struct BootstrapText
148148
dialogCodeException = "Code exception, please contact customer service",
149149
dialogFunctionCallFailed = "Function call failed: {0}",
150150
};
151+
152+
/// <summary>
153+
/// Safe wrapper around <see cref="string.Format(string, object[])"/> that falls back
154+
/// to the raw template if the user-edited format string is malformed.
155+
/// </summary>
156+
public static string SafeFormat(string template, params object[] args)
157+
{
158+
try
159+
{
160+
return string.Format(template, args);
161+
}
162+
catch (FormatException)
163+
{
164+
return template;
165+
}
166+
}
151167
}
152168
}

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -450,54 +450,54 @@ private static VisualElement CreateTextSettingsSection()
450450

451451
// Package Initialization Status
452452
AddTextSubHeader(section, "Package Initialization Status");
453-
AddTextField(section, textProperty, "initializingPackage", "Initializing");
454-
AddTextField(section, textProperty, "gettingVersion", "Getting Version");
455-
AddTextField(section, textProperty, "updatingManifest", "Updating Manifest");
456-
AddTextField(section, textProperty, "checkingUpdate", "Checking Update");
457-
AddTextField(section, textProperty, "downloadingResources", "Downloading");
458-
AddTextField(section, textProperty, "packageCompleted", "Completed");
459-
AddTextField(section, textProperty, "initializationFailed", "Failed");
460-
AddTextField(section, textProperty, "unknownPackageStatus", "Unknown Status");
453+
AddTextField(section, textProperty, nameof(BootstrapText.initializingPackage), "Initializing");
454+
AddTextField(section, textProperty, nameof(BootstrapText.gettingVersion), "Getting Version");
455+
AddTextField(section, textProperty, nameof(BootstrapText.updatingManifest), "Updating Manifest");
456+
AddTextField(section, textProperty, nameof(BootstrapText.checkingUpdate), "Checking Update");
457+
AddTextField(section, textProperty, nameof(BootstrapText.downloadingResources), "Downloading");
458+
AddTextField(section, textProperty, nameof(BootstrapText.packageCompleted), "Completed");
459+
AddTextField(section, textProperty, nameof(BootstrapText.initializationFailed), "Failed");
460+
AddTextField(section, textProperty, nameof(BootstrapText.unknownPackageStatus), "Unknown Status");
461461

462462
// Scene Load Status
463463
AddTextSubHeader(section, "Scene Load Status");
464-
AddTextField(section, textProperty, "sceneLoading", "Loading");
465-
AddTextField(section, textProperty, "sceneCompleted", "Completed");
466-
AddTextField(section, textProperty, "sceneFailed", "Failed");
467-
AddTextField(section, textProperty, "unknownSceneStatus", "Unknown Status");
464+
AddTextField(section, textProperty, nameof(BootstrapText.sceneLoading), "Loading");
465+
AddTextField(section, textProperty, nameof(BootstrapText.sceneCompleted), "Completed");
466+
AddTextField(section, textProperty, nameof(BootstrapText.sceneFailed), "Failed");
467+
AddTextField(section, textProperty, nameof(BootstrapText.unknownSceneStatus), "Unknown Status");
468468

469469
// Inline Status
470470
AddTextSubHeader(section, "Inline Status");
471-
AddTextField(section, textProperty, "initializing", "Initializing");
472-
AddTextField(section, textProperty, "downloading", "Downloading");
473-
AddTextField(section, textProperty, "downloadCompletedLoading", "Download Done");
474-
AddTextField(section, textProperty, "loadingCode", "Loading Code");
475-
AddTextField(section, textProperty, "decryptingResources", "Decrypting");
476-
AddTextField(section, textProperty, "loadingScene", "Loading Scene");
471+
AddTextField(section, textProperty, nameof(BootstrapText.initializing), "Initializing");
472+
AddTextField(section, textProperty, nameof(BootstrapText.downloading), "Downloading");
473+
AddTextField(section, textProperty, nameof(BootstrapText.downloadCompletedLoading), "Download Done");
474+
AddTextField(section, textProperty, nameof(BootstrapText.loadingCode), "Loading Code");
475+
AddTextField(section, textProperty, nameof(BootstrapText.decryptingResources), "Decrypting");
476+
AddTextField(section, textProperty, nameof(BootstrapText.loadingScene), "Loading Scene");
477477

478478
// Dialog Titles
479479
AddTextSubHeader(section, "Dialog Titles");
480-
AddTextField(section, textProperty, "dialogTitleError", "Error Title");
481-
AddTextField(section, textProperty, "dialogTitleWarning", "Warning Title");
482-
AddTextField(section, textProperty, "dialogTitleNotice", "Notice Title");
480+
AddTextField(section, textProperty, nameof(BootstrapText.dialogTitleError), "Error Title");
481+
AddTextField(section, textProperty, nameof(BootstrapText.dialogTitleWarning), "Warning Title");
482+
AddTextField(section, textProperty, nameof(BootstrapText.dialogTitleNotice), "Notice Title");
483483

484484
// Dialog Buttons
485485
AddTextSubHeader(section, "Dialog Buttons");
486-
AddTextField(section, textProperty, "buttonOk", "OK");
487-
AddTextField(section, textProperty, "buttonCancel", "Cancel");
488-
AddTextField(section, textProperty, "buttonDownload", "Download");
489-
AddTextField(section, textProperty, "buttonRetry", "Retry");
490-
AddTextField(section, textProperty, "buttonExit", "Exit");
486+
AddTextField(section, textProperty, nameof(BootstrapText.buttonOk), "OK");
487+
AddTextField(section, textProperty, nameof(BootstrapText.buttonCancel), "Cancel");
488+
AddTextField(section, textProperty, nameof(BootstrapText.buttonDownload), "Download");
489+
AddTextField(section, textProperty, nameof(BootstrapText.buttonRetry), "Retry");
490+
AddTextField(section, textProperty, nameof(BootstrapText.buttonExit), "Exit");
491491

492492
// Dialog Content
493493
AddTextSubHeader(section, "Dialog Content (Format Strings)");
494-
AddTextField(section, textProperty, "dialogInitFailed", "Init Failed");
495-
AddTextField(section, textProperty, "dialogDownloadPrompt", "Download Prompt");
496-
AddTextField(section, textProperty, "dialogDownloadProgress", "Download Progress");
497-
AddTextField(section, textProperty, "dialogSceneLoadFailed", "Scene Failed");
498-
AddTextField(section, textProperty, "dialogInitException", "Init Exception");
499-
AddTextField(section, textProperty, "dialogCodeException", "Code Exception");
500-
AddTextField(section, textProperty, "dialogFunctionCallFailed", "Call Failed");
494+
AddTextField(section, textProperty, nameof(BootstrapText.dialogInitFailed), "Init Failed");
495+
AddTextField(section, textProperty, nameof(BootstrapText.dialogDownloadPrompt), "Download Prompt");
496+
AddTextField(section, textProperty, nameof(BootstrapText.dialogDownloadProgress), "Download Progress");
497+
AddTextField(section, textProperty, nameof(BootstrapText.dialogSceneLoadFailed), "Scene Failed");
498+
AddTextField(section, textProperty, nameof(BootstrapText.dialogInitException), "Init Exception");
499+
AddTextField(section, textProperty, nameof(BootstrapText.dialogCodeException), "Code Exception");
500+
AddTextField(section, textProperty, nameof(BootstrapText.dialogFunctionCallFailed), "Call Failed");
501501

502502
// Reset to Defaults button
503503
var resetButton = new JButton("Reset to Defaults", () =>
@@ -540,12 +540,8 @@ private static void AddTextField(JSection section, SerializedProperty parentProp
540540
string fieldName, string label)
541541
{
542542
var prop = parentProperty.FindPropertyRelative(fieldName);
543-
var textField = new JTextField(prop.stringValue);
544-
textField.RegisterValueChangedCallback(evt =>
545-
{
546-
prop.stringValue = evt.newValue;
547-
_serializedObject.ApplyModifiedProperties();
548-
});
543+
var textField = new JTextField();
544+
textField.BindProperty(prop);
549545
section.Add(new JFormField(label, textField));
550546
}
551547

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/BootstrapEditorUITests.cs

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,16 +165,39 @@ public void CreateInspector_ContainsTextSettingsSection()
165165
}
166166

167167
[Test]
168-
public void TextSettings_ContainsFormFields()
168+
public void TextSettings_ContainsExactFieldCount()
169169
{
170170
var root = BootstrapEditorUI.CreateInspector(_serializedObject, _bootstrap);
171171

172172
var section = FindSectionByTitle(root, "Text Settings");
173173
var formFields = section?.Query<JFormField>().ToList();
174174

175175
Assert.IsNotNull(formFields);
176-
// Should have all 30 text fields
177-
Assert.GreaterOrEqual(formFields.Count, 30);
176+
// Must match BootstrapText public instance field count exactly
177+
var expectedCount = typeof(BootstrapText)
178+
.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
179+
.Length;
180+
Assert.AreEqual(expectedCount, formFields.Count);
181+
}
182+
183+
[Test]
184+
public void TextSettings_ContainsSubHeaders()
185+
{
186+
var root = BootstrapEditorUI.CreateInspector(_serializedObject, _bootstrap);
187+
188+
var section = FindSectionByTitle(root, "Text Settings");
189+
var labels = section?.Query<Label>().ToList();
190+
191+
Assert.IsNotNull(labels);
192+
bool foundPackageHeader = false;
193+
bool foundDialogHeader = false;
194+
foreach (var label in labels)
195+
{
196+
if (label.text == "Package Initialization Status") foundPackageHeader = true;
197+
if (label.text == "Dialog Content (Format Strings)") foundDialogHeader = true;
198+
}
199+
Assert.IsTrue(foundPackageHeader);
200+
Assert.IsTrue(foundDialogHeader);
178201
}
179202

180203
[Test]
@@ -186,7 +209,46 @@ public void TextSettings_ContainsResetButton()
186209
var buttons = section?.Query<JButton>().ToList();
187210

188211
Assert.IsNotNull(buttons);
189-
Assert.GreaterOrEqual(buttons.Count, 1);
212+
Assert.AreEqual(1, buttons.Count);
213+
}
214+
215+
[Test]
216+
public void TextSettings_ResetLogic_RestoresDefaults()
217+
{
218+
// Modify a text value via serialized property
219+
var textProp = _serializedObject.FindProperty("text");
220+
var initProp = textProp.FindPropertyRelative(nameof(BootstrapText.initializing));
221+
initProp.stringValue = "Custom text";
222+
_serializedObject.ApplyModifiedProperties();
223+
224+
// Verify value was changed
225+
_serializedObject.Update();
226+
textProp = _serializedObject.FindProperty("text");
227+
initProp = textProp.FindPropertyRelative(nameof(BootstrapText.initializing));
228+
Assert.AreEqual("Custom text", initProp.stringValue);
229+
230+
// Apply reset logic (same as Reset button callback)
231+
var defaults = BootstrapText.Default;
232+
var fields = typeof(BootstrapText).GetFields(
233+
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
234+
foreach (var field in fields)
235+
{
236+
var prop = textProp.FindPropertyRelative(field.Name);
237+
if (prop != null && prop.propertyType == SerializedPropertyType.String)
238+
{
239+
prop.stringValue = (string)field.GetValue(defaults);
240+
}
241+
}
242+
_serializedObject.ApplyModifiedProperties();
243+
244+
// Verify all values were reset
245+
_serializedObject.Update();
246+
textProp = _serializedObject.FindProperty("text");
247+
initProp = textProp.FindPropertyRelative(nameof(BootstrapText.initializing));
248+
Assert.AreEqual(BootstrapText.Default.initializing, initProp.stringValue);
249+
250+
var dlgProp = textProp.FindPropertyRelative(nameof(BootstrapText.dialogTitleError));
251+
Assert.AreEqual(BootstrapText.Default.dialogTitleError, dlgProp.stringValue);
190252
}
191253

192254
#endregion
@@ -574,6 +636,72 @@ public void CreateInspector_MultipleCalls_EachReturnsValidRoot()
574636

575637
#endregion
576638

639+
#region BootstrapText SafeFormat Tests
640+
641+
[Test]
642+
public void SafeFormat_ValidTemplate_FormatsCorrectly()
643+
{
644+
var result = BootstrapText.SafeFormat("Hello {0}, you have {1} items", "World", 5);
645+
Assert.AreEqual("Hello World, you have 5 items", result);
646+
}
647+
648+
[Test]
649+
public void SafeFormat_MalformedTemplate_ReturnsFallback()
650+
{
651+
var result = BootstrapText.SafeFormat("Bad format {0} {1} {2}", "only_one_arg");
652+
Assert.AreEqual("Bad format {0} {1} {2}", result);
653+
}
654+
655+
[Test]
656+
public void SafeFormat_InvalidBraces_ReturnsFallback()
657+
{
658+
var result = BootstrapText.SafeFormat("Broken {", "arg");
659+
Assert.AreEqual("Broken {", result);
660+
}
661+
662+
[Test]
663+
public void SafeFormat_NoPlaceholders_ReturnsTemplate()
664+
{
665+
var result = BootstrapText.SafeFormat("No placeholders here");
666+
Assert.AreEqual("No placeholders here", result);
667+
}
668+
669+
[Test]
670+
public void SafeFormat_EmptyTemplate_ReturnsEmpty()
671+
{
672+
var result = BootstrapText.SafeFormat("", "arg");
673+
Assert.AreEqual("", result);
674+
}
675+
676+
#endregion
677+
678+
#region BootstrapText Default Tests
679+
680+
[Test]
681+
public void Default_AllFieldsAreNonNull()
682+
{
683+
var defaults = BootstrapText.Default;
684+
var fields = typeof(BootstrapText).GetFields(
685+
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
686+
687+
foreach (var field in fields)
688+
{
689+
var value = (string)field.GetValue(defaults);
690+
Assert.IsNotNull(value, $"BootstrapText.Default.{field.Name} is null");
691+
Assert.IsNotEmpty(value, $"BootstrapText.Default.{field.Name} is empty");
692+
}
693+
}
694+
695+
[Test]
696+
public void Default_HasExpectedFieldCount()
697+
{
698+
var fields = typeof(BootstrapText).GetFields(
699+
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
700+
Assert.AreEqual(33, fields.Length);
701+
}
702+
703+
#endregion
704+
577705
#region Helper Methods
578706

579707
private static JSection FindSectionByTitle(VisualElement root, string title)

0 commit comments

Comments
 (0)