diff --git a/src/modules/bmgd/gametest/knowledge/e2e-testing.md b/src/modules/bmgd/gametest/knowledge/e2e-testing.md
index 4ed33d2f..8f35bcd7 100644
--- a/src/modules/bmgd/gametest/knowledge/e2e-testing.md
+++ b/src/modules/bmgd/gametest/knowledge/e2e-testing.md
@@ -472,6 +472,8 @@ public static class AsyncAssert
///
/// Wait for a specific value, with descriptive failure.
+ /// Note: For floating-point comparisons, use WaitForValueApprox instead
+ /// to handle precision issues. This method uses exact equality.
///
public static IEnumerator WaitForValue(
Func getter,
@@ -484,7 +486,39 @@ public static class AsyncAssert
$"{description} to equal {expected} (current: {getter()})",
timeout);
}
-
+
+ ///
+ /// Wait for a float value within tolerance (handles floating-point precision).
+ ///
+ public static IEnumerator WaitForValueApprox(
+ Func getter,
+ float expected,
+ string description,
+ float tolerance = 0.0001f,
+ float timeout = 5f)
+ {
+ yield return WaitUntil(
+ () => Mathf.Abs(expected - getter()) < tolerance,
+ $"{description} to equal ~{expected} ±{tolerance} (current: {getter()})",
+ timeout);
+ }
+
+ ///
+ /// Wait for a double value within tolerance (handles floating-point precision).
+ ///
+ public static IEnumerator WaitForValueApprox(
+ Func getter,
+ double expected,
+ string description,
+ double tolerance = 0.0001,
+ float timeout = 5f)
+ {
+ yield return WaitUntil(
+ () => Math.Abs(expected - getter()) < tolerance,
+ $"{description} to equal ~{expected} ±{tolerance} (current: {getter()})",
+ timeout);
+ }
+
///
/// Wait for an event to fire.
///
@@ -642,7 +676,22 @@ public IEnumerator SaveLoad_PreservesGameState()
"Movement points should be preserved");
// Cleanup
- System.IO.File.Delete(GameState.GetSavePath(savePath));
+ var savedFilePath = GameState.GetSavePath(savePath);
+ if (System.IO.File.Exists(savedFilePath))
+ {
+ try
+ {
+ System.IO.File.Delete(savedFilePath);
+ }
+ catch (System.IO.IOException ex)
+ {
+ Debug.LogWarning($"[E2E] Failed to delete test save file '{savedFilePath}': {ex.Message}");
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ Debug.LogWarning($"[E2E] Access denied deleting test save file '{savedFilePath}': {ex.Message}");
+ }
+ }
}
```
@@ -744,12 +793,17 @@ Tests/
{
"name": "E2E",
"references": [
- "GameAssembly",
- "UnityEngine.TestRunner",
- "UnityEditor.TestRunner"
+ "GameAssembly"
],
"includePlatforms": [],
"excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": true,
+ "precompiledReferences": [
+ "nunit.framework.dll",
+ "UnityEngine.TestRunner.dll",
+ "UnityEditor.TestRunner.dll"
+ ],
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
@@ -796,16 +850,18 @@ Capture screenshots and logs on failure:
[UnityTearDown]
public IEnumerator CaptureOnFailure()
{
+ // Yield first to ensure we're on the main thread for screenshot capture
+ yield return null;
+
if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)
{
var screenshot = ScreenCapture.CaptureScreenshotAsTexture();
var bytes = screenshot.EncodeToPNG();
- var path = $"TestResults/Screenshots/{TestContext.CurrentContext.Test.Name}.png";
- System.IO.File.WriteAllBytes(path, bytes);
-
- Debug.Log($"[E2E FAILURE] Screenshot saved: {path}");
+ var screenshotPath = $"TestResults/Screenshots/{TestContext.CurrentContext.Test.Name}.png";
+ System.IO.File.WriteAllBytes(screenshotPath, bytes);
+
+ Debug.Log($"[E2E FAILURE] Screenshot saved: {screenshotPath}");
}
- yield return null;
}
```
diff --git a/src/modules/bmgd/workflows/gametest/e2e-scaffold/checklist.md b/src/modules/bmgd/workflows/gametest/e2e-scaffold/checklist.md
index 7f4a4b1f..58a510d2 100644
--- a/src/modules/bmgd/workflows/gametest/e2e-scaffold/checklist.md
+++ b/src/modules/bmgd/workflows/gametest/e2e-scaffold/checklist.md
@@ -67,11 +67,12 @@
## Assembly Definition
- [ ] References main game assembly
-- [ ] References UnityEngine.TestRunner
-- [ ] References UnityEditor.TestRunner
- [ ] References Unity.InputSystem (if applicable)
+- [ ] `overrideReferences` set to true
+- [ ] `precompiledReferences` includes nunit.framework.dll
+- [ ] `precompiledReferences` includes UnityEngine.TestRunner.dll
+- [ ] `precompiledReferences` includes UnityEditor.TestRunner.dll
- [ ] `UNITY_INCLUDE_TESTS` define constraint set
-- [ ] nunit.framework.dll in precompiled references
## Verification
diff --git a/src/modules/bmgd/workflows/gametest/e2e-scaffold/instructions.md b/src/modules/bmgd/workflows/gametest/e2e-scaffold/instructions.md
index efab44a7..42b99840 100644
--- a/src/modules/bmgd/workflows/gametest/e2e-scaffold/instructions.md
+++ b/src/modules/bmgd/workflows/gametest/e2e-scaffold/instructions.md
@@ -122,8 +122,6 @@ Tests/PlayMode/E2E/
"rootNamespace": "{ProjectNamespace}.Tests.E2E",
"references": [
"{GameAssemblyName}",
- "UnityEngine.TestRunner",
- "UnityEditor.TestRunner",
"Unity.InputSystem",
"Unity.InputSystem.TestFramework"
],
@@ -132,7 +130,9 @@ Tests/PlayMode/E2E/
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
- "nunit.framework.dll"
+ "nunit.framework.dll",
+ "UnityEngine.TestRunner.dll",
+ "UnityEditor.TestRunner.dll"
],
"autoReferenced": false,
"defineConstraints": [
@@ -474,24 +474,30 @@ namespace {Namespace}.Tests.E2E
{
var button = GameObject.Find(buttonName)?
.GetComponent();
-
+
if (button == null)
{
- // Try searching in inactive objects
- var buttons = Resources.FindObjectsOfTypeAll();
+ // Search in inactive objects within loaded scenes only
+ var buttons = Object.FindObjectsByType(
+ FindObjectsInactive.Include, FindObjectsSortMode.None);
foreach (var b in buttons)
{
- if (b.name == buttonName)
+ if (b.name == buttonName && b.gameObject.scene.isLoaded)
{
button = b;
break;
}
}
}
-
- UnityEngine.Assertions.Assert.IsNotNull(button,
- $"Button '{buttonName}' not found");
-
+
+ UnityEngine.Assertions.Assert.IsNotNull(button,
+ $"Button '{buttonName}' not found in active scenes");
+
+ if (!button.interactable)
+ {
+ Debug.LogWarning($"[InputSimulator] Button '{buttonName}' is not interactable");
+ }
+
button.onClick.Invoke();
yield return null;
}
@@ -697,6 +703,8 @@ namespace {Namespace}.Tests.E2E
///
/// Wait for a value to equal expected.
+ /// Note: For floating-point comparisons, use WaitForValueApprox instead
+ /// to handle precision issues. This method uses exact equality.
///
public static IEnumerator WaitForValue(
Func getter,
@@ -709,7 +717,39 @@ namespace {Namespace}.Tests.E2E
$"{description} to equal '{expected}' (current: '{getter()}')",
timeout);
}
-
+
+ ///
+ /// Wait for a float value within tolerance (handles floating-point precision).
+ ///
+ public static IEnumerator WaitForValueApprox(
+ Func getter,
+ float expected,
+ string description,
+ float tolerance = 0.0001f,
+ float timeout = 5f)
+ {
+ yield return WaitUntil(
+ () => Mathf.Abs(expected - getter()) < tolerance,
+ $"{description} to equal ~{expected} ±{tolerance} (current: {getter()})",
+ timeout);
+ }
+
+ ///
+ /// Wait for a double value within tolerance (handles floating-point precision).
+ ///
+ public static IEnumerator WaitForValueApprox(
+ Func getter,
+ double expected,
+ string description,
+ double tolerance = 0.0001,
+ float timeout = 5f)
+ {
+ yield return WaitUntil(
+ () => Math.Abs(expected - getter()) < tolerance,
+ $"{description} to equal ~{expected} ±{tolerance} (current: {getter()})",
+ timeout);
+ }
+
///
/// Wait for a value to not equal a specific value.
///
@@ -835,6 +875,8 @@ namespace {Namespace}.Tests.E2E
Assert.IsNotNull(Scenario, "ScenarioBuilder should be available");
// Verify game is actually ready
+ // NOTE: {IsReadyProperty} is a template placeholder. Replace it with your
+ // game's actual ready-state property (e.g., IsReady, IsInitialized, HasLoaded).
yield return AsyncAssert.WaitUntil(
() => GameState.{IsReadyProperty},
"Game should be in ready state");
diff --git a/src/modules/bmgd/workflows/gametest/e2e-scaffold/workflow.yaml b/src/modules/bmgd/workflows/gametest/e2e-scaffold/workflow.yaml
index 013be903..03d7c465 100644
--- a/src/modules/bmgd/workflows/gametest/e2e-scaffold/workflow.yaml
+++ b/src/modules/bmgd/workflows/gametest/e2e-scaffold/workflow.yaml
@@ -24,23 +24,24 @@ workflow:
- "Game has identifiable state manager"
- "Main gameplay scene exists"
+ # Paths are relative to this workflow file's location
knowledge_fragments:
- - "knowledge/e2e-testing.md"
- - "knowledge/unity-testing.md"
- - "knowledge/unreal-testing.md"
- - "knowledge/godot-testing.md"
+ - "../../../gametest/knowledge/e2e-testing.md"
+ - "../../../gametest/knowledge/unity-testing.md"
+ - "../../../gametest/knowledge/unreal-testing.md"
+ - "../../../gametest/knowledge/godot-testing.md"
inputs:
game_state_class:
description: "Primary game state manager class name"
required: true
example: "GameStateManager"
-
+
main_scene:
description: "Scene name where core gameplay occurs"
required: true
example: "GameScene"
-
+
input_system:
description: "Input system in use"
required: false
@@ -52,47 +53,90 @@ workflow:
- "godot-input"
- "custom"
+ # Output paths vary by engine. Generate files matching detected engine.
outputs:
- infrastructure_files:
- description: "Generated E2E infrastructure classes"
- files:
- - "Tests/PlayMode/E2E/Infrastructure/GameE2ETestFixture.cs"
- - "Tests/PlayMode/E2E/Infrastructure/ScenarioBuilder.cs"
- - "Tests/PlayMode/E2E/Infrastructure/InputSimulator.cs"
- - "Tests/PlayMode/E2E/Infrastructure/AsyncAssert.cs"
-
- assembly_definition:
- description: "E2E test assembly configuration"
- files:
- - "Tests/PlayMode/E2E/E2E.asmdef"
-
- example_test:
- description: "Working example E2E test"
- files:
- - "Tests/PlayMode/E2E/ExampleE2ETest.cs"
-
- documentation:
- description: "E2E testing README"
- files:
- - "Tests/PlayMode/E2E/README.md"
+ unity:
+ condition: "engine == 'unity'"
+ infrastructure_files:
+ description: "Generated E2E infrastructure classes"
+ files:
+ - "Tests/PlayMode/E2E/Infrastructure/GameE2ETestFixture.cs"
+ - "Tests/PlayMode/E2E/Infrastructure/ScenarioBuilder.cs"
+ - "Tests/PlayMode/E2E/Infrastructure/InputSimulator.cs"
+ - "Tests/PlayMode/E2E/Infrastructure/AsyncAssert.cs"
+ assembly_definition:
+ description: "E2E test assembly configuration"
+ files:
+ - "Tests/PlayMode/E2E/E2E.asmdef"
+ example_test:
+ description: "Working example E2E test"
+ files:
+ - "Tests/PlayMode/E2E/ExampleE2ETest.cs"
+ documentation:
+ description: "E2E testing README"
+ files:
+ - "Tests/PlayMode/E2E/README.md"
+
+ unreal:
+ condition: "engine == 'unreal'"
+ infrastructure_files:
+ description: "Generated E2E infrastructure classes"
+ files:
+ - "Source/{ProjectName}/Tests/E2E/GameE2ETestBase.h"
+ - "Source/{ProjectName}/Tests/E2E/GameE2ETestBase.cpp"
+ - "Source/{ProjectName}/Tests/E2E/ScenarioBuilder.h"
+ - "Source/{ProjectName}/Tests/E2E/ScenarioBuilder.cpp"
+ - "Source/{ProjectName}/Tests/E2E/InputSimulator.h"
+ - "Source/{ProjectName}/Tests/E2E/InputSimulator.cpp"
+ - "Source/{ProjectName}/Tests/E2E/AsyncAssert.h"
+ build_configuration:
+ description: "E2E test build configuration"
+ files:
+ - "Source/{ProjectName}/Tests/E2E/{ProjectName}E2ETests.Build.cs"
+ example_test:
+ description: "Working example E2E test"
+ files:
+ - "Source/{ProjectName}/Tests/E2E/ExampleE2ETest.cpp"
+ documentation:
+ description: "E2E testing README"
+ files:
+ - "Source/{ProjectName}/Tests/E2E/README.md"
+
+ godot:
+ condition: "engine == 'godot'"
+ infrastructure_files:
+ description: "Generated E2E infrastructure classes"
+ files:
+ - "tests/e2e/infrastructure/game_e2e_test_fixture.gd"
+ - "tests/e2e/infrastructure/scenario_builder.gd"
+ - "tests/e2e/infrastructure/input_simulator.gd"
+ - "tests/e2e/infrastructure/async_assert.gd"
+ example_test:
+ description: "Working example E2E test"
+ files:
+ - "tests/e2e/scenarios/example_e2e_test.gd"
+ documentation:
+ description: "E2E testing README"
+ files:
+ - "tests/e2e/README.md"
steps:
- id: analyze
name: "Analyze Game Architecture"
instruction_file: "instructions.md#step-1-analyze-game-architecture"
-
+
- id: scaffold
name: "Generate Infrastructure"
instruction_file: "instructions.md#step-2-generate-infrastructure"
-
+
- id: example
name: "Generate Example Test"
instruction_file: "instructions.md#step-3-generate-example-test"
-
+
- id: document
name: "Generate Documentation"
instruction_file: "instructions.md#step-4-generate-documentation"
-
+
- id: complete
name: "Output Summary"
instruction_file: "instructions.md#step-5-output-summary"