Skip to content

Commit 4306e9f

Browse files
feat(util): add parallel execution support to JAction
- Add JActionExecutionContext for per-execution state isolation - Add JActionExecution result struct with per-execution Cancelled state - Add JActionExecutionHandle for per-execution cancellation control - Execute/ExecuteAsync now snapshot tasks enabling safe parallel execution - Switch from ValueTask to UniTask for better Unity integration - Add comprehensive tests for parallel execution, timeouts, and edge cases - Add MessageBox test hooks for pool state inspection - Update jaction skill documentation with new API and patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8d8ff35 commit 4306e9f

17 files changed

Lines changed: 2304 additions & 632 deletions

File tree

.claude-plugin/skills/jaction/SKILL.md

Lines changed: 203 additions & 131 deletions
Large diffs are not rendered by default.

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,86 @@ private static GameObject Prefab
124124
internal static bool SimulateNoPrefab;
125125
#endif
126126

127+
#if UNITY_INCLUDE_TESTS
128+
/// <summary>
129+
/// Test hook: Gets the pool state for verification in tests.
130+
/// Returns (activeCount, pooledCount).
131+
/// </summary>
132+
internal static (int activeCount, int pooledCount) TestGetPoolState()
133+
{
134+
return (ActiveMessageBoxes.Count, PooledMessageBoxes.Count);
135+
}
136+
137+
/// <summary>
138+
/// Test hook: Simulates clicking a button on the most recently shown message box.
139+
/// </summary>
140+
/// <param name="clickOk">If true, simulates clicking OK; otherwise simulates clicking Cancel.</param>
141+
/// <returns>True if a message box was found and the click was simulated.</returns>
142+
internal static bool TestSimulateButtonClick(bool clickOk)
143+
{
144+
if (ActiveMessageBoxes.Count == 0) return false;
145+
146+
// Get the most recent message box (any will do for testing)
147+
MessageBox target = null;
148+
foreach (var box in ActiveMessageBoxes)
149+
{
150+
target = box;
151+
break;
152+
}
153+
154+
if (target == null) return false;
155+
156+
target.HandleEvent(clickOk);
157+
return true;
158+
}
159+
160+
/// <summary>
161+
/// Test hook: Gets the button visibility state of the most recently shown message box.
162+
/// </summary>
163+
/// <returns>Tuple of (okButtonVisible, noButtonVisible), or null if no active boxes.</returns>
164+
internal static (bool okVisible, bool noVisible)? TestGetButtonVisibility()
165+
{
166+
if (ActiveMessageBoxes.Count == 0) return null;
167+
168+
MessageBox target = null;
169+
foreach (var box in ActiveMessageBoxes)
170+
{
171+
target = box;
172+
break;
173+
}
174+
175+
if (target == null || target._buttonOk == null || target._buttonNo == null)
176+
return null;
177+
178+
return (target._buttonOk.gameObject.activeSelf, target._buttonNo.gameObject.activeSelf);
179+
}
180+
181+
/// <summary>
182+
/// Test hook: Gets the text content of the most recently shown message box.
183+
/// </summary>
184+
/// <returns>Tuple of (title, content, okText, noText), or null if no active boxes.</returns>
185+
internal static (string title, string content, string okText, string noText)? TestGetContent()
186+
{
187+
if (ActiveMessageBoxes.Count == 0) return null;
188+
189+
MessageBox target = null;
190+
foreach (var box in ActiveMessageBoxes)
191+
{
192+
target = box;
193+
break;
194+
}
195+
196+
if (target == null) return null;
197+
198+
return (
199+
target._title?.text,
200+
target._content?.text,
201+
target._textOk?.text,
202+
target._textNo?.text
203+
);
204+
}
205+
#endif
206+
127207
private TextMeshProUGUI _content;
128208
private TextMeshProUGUI _textNo;
129209
private TextMeshProUGUI _textOk;

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/MessageBoxTests.cs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,5 +274,259 @@ public IEnumerator Show_MultipleCalls_AllReturnFalse_WhenNoPrefab() => UniTask.T
274274
});
275275

276276
#endregion
277+
278+
#region Pool State Tests (using test hooks)
279+
280+
[UnityTest]
281+
public IEnumerator Show_IncrementsActiveCount_WhenUsingTestHandler() => UniTask.ToCoroutine(async () =>
282+
{
283+
// Note: When TestHandler is set, the actual UI is bypassed,
284+
// so ActiveCount won't increase. This test verifies the expected behavior.
285+
MessageBox.TestHandler = (_, _, _, _) => UniTask.FromResult(true);
286+
287+
var (initialActive, _) = MessageBox.TestGetPoolState();
288+
await MessageBox.Show("Test", "Content");
289+
var (finalActive, _) = MessageBox.TestGetPoolState();
290+
291+
// With TestHandler, no actual MessageBox is created
292+
Assert.AreEqual(initialActive, finalActive);
293+
});
294+
295+
[Test]
296+
public void TestGetPoolState_ReturnsCorrectInitialState()
297+
{
298+
var (activeCount, pooledCount) = MessageBox.TestGetPoolState();
299+
300+
Assert.AreEqual(0, activeCount);
301+
Assert.AreEqual(0, pooledCount);
302+
}
303+
304+
[Test]
305+
public void TestGetPoolState_AfterDispose_ReturnsZero()
306+
{
307+
MessageBox.Dispose();
308+
309+
var (activeCount, pooledCount) = MessageBox.TestGetPoolState();
310+
311+
Assert.AreEqual(0, activeCount);
312+
Assert.AreEqual(0, pooledCount);
313+
}
314+
315+
[Test]
316+
public void TestSimulateButtonClick_ReturnsFalse_WhenNoActiveBoxes()
317+
{
318+
bool result = MessageBox.TestSimulateButtonClick(true);
319+
320+
Assert.IsFalse(result);
321+
}
322+
323+
[Test]
324+
public void TestGetButtonVisibility_ReturnsNull_WhenNoActiveBoxes()
325+
{
326+
var result = MessageBox.TestGetButtonVisibility();
327+
328+
Assert.IsNull(result);
329+
}
330+
331+
[Test]
332+
public void TestGetContent_ReturnsNull_WhenNoActiveBoxes()
333+
{
334+
var result = MessageBox.TestGetContent();
335+
336+
Assert.IsNull(result);
337+
}
338+
339+
#endregion
340+
341+
#region Button Visibility Tests
342+
343+
[UnityTest]
344+
public IEnumerator Show_EmptyOkText_PassesToHandler() => UniTask.ToCoroutine(async () =>
345+
{
346+
string receivedOk = "not-empty";
347+
348+
MessageBox.TestHandler = (_, _, ok, _) =>
349+
{
350+
receivedOk = ok;
351+
return UniTask.FromResult(true);
352+
};
353+
354+
await MessageBox.Show("Title", "Content", "", "Cancel");
355+
356+
// Empty string is passed through
357+
Assert.AreEqual("", receivedOk);
358+
});
359+
360+
[UnityTest]
361+
public IEnumerator Show_EmptyNoText_PassesToHandler() => UniTask.ToCoroutine(async () =>
362+
{
363+
string receivedNo = "not-empty";
364+
365+
MessageBox.TestHandler = (_, _, _, no) =>
366+
{
367+
receivedNo = no;
368+
return UniTask.FromResult(true);
369+
};
370+
371+
await MessageBox.Show("Title", "Content", "OK", "");
372+
373+
// Empty string is passed through
374+
Assert.AreEqual("", receivedNo);
375+
});
376+
377+
[UnityTest]
378+
public IEnumerator Show_NullOkText_PassesToHandler() => UniTask.ToCoroutine(async () =>
379+
{
380+
string receivedOk = "not-null";
381+
382+
MessageBox.TestHandler = (_, _, ok, _) =>
383+
{
384+
receivedOk = ok;
385+
return UniTask.FromResult(true);
386+
};
387+
388+
await MessageBox.Show("Title", "Content", null, "Cancel");
389+
390+
// Null is passed through
391+
Assert.IsNull(receivedOk);
392+
});
393+
394+
[UnityTest]
395+
public IEnumerator Show_NullNoText_PassesToHandler() => UniTask.ToCoroutine(async () =>
396+
{
397+
string receivedNo = "not-null";
398+
399+
MessageBox.TestHandler = (_, _, _, no) =>
400+
{
401+
receivedNo = no;
402+
return UniTask.FromResult(true);
403+
};
404+
405+
await MessageBox.Show("Title", "Content", "OK", null);
406+
407+
// Null is passed through
408+
Assert.IsNull(receivedNo);
409+
});
410+
411+
[UnityTest]
412+
public IEnumerator Show_BothButtonsNullOrEmpty_DefaultsToOkInHandler() => UniTask.ToCoroutine(async () =>
413+
{
414+
// Note: The safety check for both buttons being empty happens AFTER
415+
// TestHandler is checked, so TestHandler receives the original null values
416+
string receivedOk = "not-null";
417+
string receivedNo = "not-null";
418+
419+
MessageBox.TestHandler = (_, _, ok, no) =>
420+
{
421+
receivedOk = ok;
422+
receivedNo = no;
423+
return UniTask.FromResult(true);
424+
};
425+
426+
await MessageBox.Show("Title", "Content", null, null);
427+
428+
// TestHandler receives original null values
429+
Assert.IsNull(receivedOk);
430+
Assert.IsNull(receivedNo);
431+
});
432+
433+
#endregion
434+
435+
#region Null Content Handling Tests
436+
437+
[UnityTest]
438+
public IEnumerator Show_NullTitle_HandledGracefully() => UniTask.ToCoroutine(async () =>
439+
{
440+
string receivedTitle = "not-null";
441+
442+
MessageBox.TestHandler = (title, _, _, _) =>
443+
{
444+
receivedTitle = title;
445+
return UniTask.FromResult(true);
446+
};
447+
448+
bool result = await MessageBox.Show(null, "Content");
449+
450+
Assert.IsNull(receivedTitle);
451+
Assert.IsTrue(result);
452+
});
453+
454+
[UnityTest]
455+
public IEnumerator Show_NullContent_HandledGracefully() => UniTask.ToCoroutine(async () =>
456+
{
457+
string receivedContent = "not-null";
458+
459+
MessageBox.TestHandler = (_, content, _, _) =>
460+
{
461+
receivedContent = content;
462+
return UniTask.FromResult(true);
463+
};
464+
465+
bool result = await MessageBox.Show("Title", null);
466+
467+
Assert.IsNull(receivedContent);
468+
Assert.IsTrue(result);
469+
});
470+
471+
[UnityTest]
472+
public IEnumerator Show_EmptyStrings_HandledGracefully() => UniTask.ToCoroutine(async () =>
473+
{
474+
string receivedTitle = null;
475+
string receivedContent = null;
476+
477+
MessageBox.TestHandler = (title, content, _, _) =>
478+
{
479+
receivedTitle = title;
480+
receivedContent = content;
481+
return UniTask.FromResult(true);
482+
};
483+
484+
bool result = await MessageBox.Show("", "");
485+
486+
Assert.AreEqual("", receivedTitle);
487+
Assert.AreEqual("", receivedContent);
488+
Assert.IsTrue(result);
489+
});
490+
491+
#endregion
492+
493+
#region Concurrent Operations Tests
494+
495+
[UnityTest]
496+
public IEnumerator Show_MultipleConcurrent_AllComplete() => UniTask.ToCoroutine(async () =>
497+
{
498+
int completionCount = 0;
499+
500+
MessageBox.TestHandler = (_, _, _, _) =>
501+
{
502+
completionCount++;
503+
return UniTask.FromResult(true);
504+
};
505+
506+
// Show multiple message boxes concurrently
507+
var task1 = MessageBox.Show("Test1", "Content1");
508+
var task2 = MessageBox.Show("Test2", "Content2");
509+
var task3 = MessageBox.Show("Test3", "Content3");
510+
511+
await UniTask.WhenAll(task1, task2, task3);
512+
513+
Assert.AreEqual(3, completionCount);
514+
});
515+
516+
[UnityTest]
517+
public IEnumerator CloseAll_AfterMultipleShows_ClearsAll() => UniTask.ToCoroutine(async () =>
518+
{
519+
MessageBox.TestHandler = (_, _, _, _) => UniTask.FromResult(true);
520+
521+
await MessageBox.Show("Test1", "Content1");
522+
await MessageBox.Show("Test2", "Content2");
523+
524+
MessageBox.CloseAll();
525+
526+
var (activeCount, _) = MessageBox.TestGetPoolState();
527+
Assert.AreEqual(0, activeCount);
528+
});
529+
530+
#endregion
277531
}
278532
}

0 commit comments

Comments
 (0)