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"