-
Notifications
You must be signed in to change notification settings - Fork 0
Reflection Access
Access game state via reflection using Search.Reflect(). This allows tests to validate internal game state without direct assembly references, making it useful when tests are in a separate assembly from the game code.
Static paths provide a way to access any static field, property, or nested member in your codebase:
// Access static properties
var health = Search.Reflect("Player.Instance.Health").GetValue<float>();
var score = Search.Reflect("GameManager.Instance.Score").GetValue<int>();
var isReady = Search.Reflect("GameManager.Instance.IsReady").GetValue<bool>();
// Access Unity APIs
var screenWidth = Search.Reflect("Screen.width").GetValue<int>();
var isPlaying = Search.Reflect("Application.isPlaying").GetValue<bool>();Create a Search object from a dot-separated path:
// Simple static property
var instance = Search.Reflect("GameManager.Instance");
// Nested property access
var playerHealth = Search.Reflect("GameManager.Instance.Player.Health");
// Works with any static type
var timeScale = Search.Reflect("Time.timeScale");Chain .Property() to navigate deeper into objects. You can use either chained calls or dot notation within a single call:
// Navigate to nested properties (chained)
var damageController = Search.Reflect("Player.Instance").Property("DamageController");
var maxHealth = damageController.Property("MaxHealth").GetValue<float>();
// Multiple levels of navigation (chained)
var weaponDamage = Search.Reflect("Player.Instance")
.Property("Inventory")
.Property("EquippedWeapon")
.Property("Damage")
.GetValue<float>();
// Dot notation in Property() - equivalent to chained calls
var weaponDamage2 = Search.Reflect("Player.Instance")
.Property("Inventory.EquippedWeapon.Damage")
.GetValue<float>();
// Useful when property path is dynamic or from configuration
string propertyPath = "loadedLevel.player.racingLine";
var value = Search.Reflect("GameModeDrag.Instance").Property(propertyPath).Value;.Property() also works on normal Text(), Name(), and Type() searches to access component properties via reflection. The search finds the first matching element, then .Property() accesses its properties:
// Access properties on found UI elements (finds first match)
var buttonText = new Search().Name("SubmitButton").Property("interactable").GetValue<bool>();
var sliderValue = new Search().Name("VolumeSlider").Property("value").GetValue<float>();
// Access nested component properties
var tmpColor = new Search().Name("Title").Property("color").Value;
// Access any property on any component
var rectWidth = new Search().Name("Panel").Property("rect").Property("width").GetValue<float>();
// Combine with other search methods to narrow down which element
var specificButton = new Search().Name("Submit*").Type<Button>().Property("interactable").GetValue<bool>();This is useful when you need to check a property that isn't exposed through GetValue<T>. Note that the search will grab the first matching entry, so use additional search criteria to ensure you get the right element.
Get typed values from static paths using GetValue<T>() or .Value for raw object access:
| Method / Property | Return Type | Description |
|---|---|---|
Value |
object | Raw object value |
GetValue<string>() |
string | String value |
GetValue<bool>() |
bool | Boolean value |
GetValue<int>() |
int | Integer value |
GetValue<float>() |
float | Float value |
GetValue<Vector3>() |
Vector3 | Vector3 from static path or transform position |
GetValue<Vector2>() |
Vector2 | Vector2 from static path or RectTransform position |
GetValue<Color>() |
Color | Color from static path or Image/Text color |
GetValue<Quaternion>() |
Quaternion | Quaternion from static path or transform rotation |
GetValue<T[]>() |
T[] | Array of typed values |
var path = Search.Reflect("Player.Instance");
// String
string name = path.Property("Name").GetValue<string>();
// Boolean
bool isAlive = path.Property("IsAlive").GetValue<bool>();
// Integer
int score = path.Property("Score").GetValue<int>();
// Float
float health = path.Property("Health").GetValue<float>();
// Raw object
object data = path.Property("CustomData").Value;Access Unity types directly from static paths or UI elements:
// Vector3 from static path
var position = Search.Reflect("Player.Instance.Transform.position").GetValue<Vector3>();
// Vector2 from UI element (gets RectTransform.anchoredPosition)
var uiPos = new Search().Name("Button").GetValue<Vector2>();
// Color from static path
var color = Search.Reflect("Theme.PrimaryColor").GetValue<Color>();
// Color from UI element (gets Image or Text color)
var btnColor = new Search().Name("SubmitButton").GetValue<Color>();
// Quaternion from static path
var rotation = Search.Reflect("Player.Instance.Transform.rotation").GetValue<Quaternion>();
// Quaternion from UI element (gets transform rotation)
var uiRotation = new Search().Name("RotatedPanel").GetValue<Quaternion>();Get strongly-typed values with automatic conversion:
// From Search.Reflect()
var truck = Search.Reflect("TestTrucks.PlayerTruck");
string name = truck.GetValue<string>("Name");
float health = truck.GetValue<float>("Health");
int count = truck.GetValue<int>("Count");
// For arrays
string[] tags = truck.Property("Tags").GetValue<string[]>();
int[] scores = truck.Property("Scores").GetValue<int[]>();
// Direct static path access via ActionExecutor
string playerName = ActionExecutor.GetValue<string>("Player.Instance.Name");
float playerHealth = ActionExecutor.GetValue<float>("Player.Instance.Health");Call methods on objects via reflection using .Invoke():
// Call methods on static instances
Search.Reflect("GameManager.Instance").Invoke("StartGame");
Search.Reflect("Player.Instance").Invoke("TakeDamage", 10f);
// Call static methods on a type (no instance needed)
Search.Reflect("ParseAccountAPI").Invoke("ForceInvalidateCurrentUser");
Search.Reflect("PlayerPrefs").Invoke("DeleteAll");
// Get return values with Invoke<T>
bool isValid = Search.Reflect("Validator.Instance").Invoke<bool>("Validate", inputData);
string result = Search.Reflect("Calculator.Instance").Invoke<string>("Calculate", 5, 10);
// Call methods on UI elements (finds first match)
new Search().Name("DialogBox").Invoke("Close");
new Search().Name("AudioManager").Invoke("PlaySound", "click");
// Chain with Property navigation
Search.Reflect("Player.Instance")
.Property("Inventory")
.Invoke("AddItem", "Sword", 1);
// Get return value from UI element method
int count = new Search().Name("Counter").Invoke<int>("GetCount");Call async methods and await their completion using .InvokeAsync():
// Await an async static method
await Search.Reflect("ParseAccountAPI").InvokeAsync("LogoutAsync");
// Await an async instance method
await Search.Reflect("GameManager.Instance").InvokeAsync("SaveAsync");
// Get typed return value from async method
var user = await Search.Reflect("UserService").InvokeAsync<User>("GetCurrentUserAsync");
var data = await Search.Reflect("DataLoader.Instance").InvokeAsync<string>("LoadAsync", "config.json");Methods are resolved using smart signature matching:
- Exact Match: First attempts to match method by exact argument types
- Type-Compatible Match: Falls back to scoring candidates by type compatibility (assignable types, implicit numeric conversions)
- Optional Parameters: Methods with default/optional parameters are matched — omitted args are filled with their default values
-
Static vs Instance: Automatically uses static binding when
Reflect()resolves to a type name (no instance) - Private Access: Both public and private methods are accessible
- Component Search: For UI elements, searches all components for the method
// Works with private methods
Search.Reflect("GameManager.Instance").Invoke("InternalReset");
// Works with overloaded methods (matches by argument types)
Search.Reflect("Calculator").Invoke("Add", 5, 10); // Add(int, int)
Search.Reflect("Calculator").Invoke("Add", 5.0f, 10.0f); // Add(float, float)
// Works with optional/default parameters
// Given: void Configure(string name, int retries = 3, bool verbose = false)
Search.Reflect("Server.Instance").Invoke("Configure", "main"); // uses defaults
Search.Reflect("Server.Instance").Invoke("Configure", "main", 5); // overrides retries
Search.Reflect("Server.Instance").Invoke("Configure", "main", 5, true); // overrides all
// Implicit numeric conversions (int -> long, float -> double, etc.)
// Given: void SetValue(long amount)
Search.Reflect("Counter.Instance").Invoke("SetValue", 42); // int passed to long param
// Clear error messages when no match found
// Throws: "Method 'Foo' not found on type 'Bar' matching arg types [String, Int32].
// Available signatures:
// Void Foo(String name, Float value)
// Void Foo(Int32 count)"Access array elements, list items, and dictionary values using indexers:
Use .Index(int) for arrays/lists or .Index(string) for dictionaries:
// Access array element
var firstItem = Search.Reflect("Inventory.Items").Index(0);
var itemName = firstItem.Property("Name").GetValue<string>();
// Access list element
var secondPlayer = Search.Reflect("GameManager.Players").Index(1);
var score = secondPlayer.Property("Score").GetValue<int>();
// Access dictionary by key
var settings = Search.Reflect("Config.Settings").Index("volume").GetValue<float>();
var player = Search.Reflect("PlayerManager.PlayersByName").Index("Player1");Use native C# indexer syntax for cleaner code:
// Array/list access with []
var weapon = Search.Reflect("Player.Inventory").Property("Weapons")[0];
var damage = weapon.Property("Damage").GetValue<int>();
// Dictionary access with []
var value = Search.Reflect("Config.Settings")["musicVolume"].GetValue<float>();
// Chain multiple indexers
var cell = Search.Reflect("Grid.Cells")[2][3].Property("Value").GetValue<int>();Include indexers directly in the path string:
// Array index in path
var name = Search.Reflect("Game.Players[0].Name").GetValue<string>();
// Dictionary key in path (use quotes)
var setting = Search.Reflect("Config.Settings[\"volume\"]").GetValue<float>();
// Single quotes also work
var value = Search.Reflect("Config.Settings['music']").GetValue<float>();
// Chained indexers
var cell = Search.Reflect("Grid.Data[1][2]").GetValue<int>();
// Combined with property navigation
var score = Search.Reflect("Game.Teams[0].Players[2].Score").GetValue<int>();Use indexer syntax within .Property() calls:
// Access indexed property
var item = Search.Reflect("Inventory").Property("Items[0]");
// Dictionary access in Property
var setting = Search.Reflect("Config").Property("Settings[\"audio\"]");Access nested types using dot notation (in addition to the CLR + syntax):
// These are equivalent:
Search.Reflect("GameConfig.Settings.MaxPlayers") // Dot syntax (cleaner)
Search.Reflect("GameConfig+Settings.MaxPlayers") // CLR nested type syntax
// Access static fields on nested types
var defaultHealth = Search.Reflect("PlayerStats.Defaults.Health").GetValue<float>();Static paths that resolve to arrays can be iterated:
// Iterate over array items
foreach (var truck in Search.Reflect("TruckManager.AllTrucks"))
{
var name = truck.Property("Name").GetValue<string>();
var health = truck.Property("Health").GetValue<float>();
Debug.Log($"{name}: {health} HP");
}
// Access nested properties during iteration
foreach (var player in Search.Reflect("GameManager.Players"))
{
var weaponDamage = player.Property("Weapon").Property("Damage").GetValue<float>();
}Static paths are resolved using reflection:
-
Type Resolution: The first part identifies the type (e.g.,
GameManager) -
Member Access: Subsequent parts are properties or fields (e.g.,
Instance.Score) - Private Access: Both public and private members are accessible
- Cross-Assembly: Works across all loaded assemblies
// Examples of valid paths:
"GameManager.Instance" // Static property
"GameManager.Instance.Score" // Instance property on static instance
"Player.Instance.health" // Private field (lowercase convention)
"Application.isPlaying" // Unity API
"Screen.width" // Unity property
"MyNamespace.MyClass.StaticField" // Fully qualified type nameShort type names are auto-resolved if unique across all assemblies:
// Short name (works if GameManager is unique)
Search.Reflect("GameManager.Instance")
// Fully qualified (required if multiple GameManager types exist)
Search.Reflect("MyGame.Managers.GameManager.Instance")Combine static paths with NUnit for game state validation:
// Verify instance exists
Assert.IsNotNull(Search.Reflect("GameManager.Instance").Value);
// Verify boolean state
Assert.IsTrue(Search.Reflect("GameManager.Instance.IsPlaying").GetValue<bool>());
// Verify numeric values
Assert.AreEqual(100, Search.Reflect("Player.Health").GetValue<int>());
Assert.Greater(Search.Reflect("Player.Score").GetValue<int>(), 0);
Assert.Less(Search.Reflect("Game.TimeRemaining").GetValue<float>(), 60f);
// Verify string values
Assert.AreEqual("MainLevel", Search.Reflect("LevelManager.CurrentLevel.Name").GetValue<string>());Wait for game state conditions:
// Wait for instance to exist
await WaitFor(Search.Reflect("Player.Instance"), timeout: 30f);
// Wait for boolean to become true
await WaitFor(Search.Reflect("GameManager.Instance.IsReady"));
// Wait for specific value
await WaitFor(Search.Reflect("Player.Score"), 100, timeout: 60f);
await WaitFor(Search.Reflect("Game.State"), "Playing", timeout: 10f);WaitFor(path) checks for "truthy" values:
| Type | Truthy | Falsy |
|---|---|---|
object |
not null | null |
bool |
true | false |
int, long
|
!= 0 | 0 |
float, double
|
!= 0 | 0 |
string |
not empty | null or "" |
Get a component from a GameObject and access its properties via reflection using .Component<T>() or .Component(string):
// Get Rigidbody component and set isKinematic
Search.Reflect("GameModeDrag.Instance.competitorControllers[1]")
.Component<Rigidbody>()
.Property("isKinematic")
.SetValue(true);
// String-based version (useful when type isn't available at compile time)
Search.Reflect("Player.Instance")
.Component("Rigidbody")
.Property("velocity")
.SetValue(Vector3.zero);
// From UI element searches
new Search().Name("CarController")
.Component<Rigidbody>()
.Property("isKinematic")
.SetValue(true);
// Access custom component properties
new Search().Name("Player")
.Component("PlayerStats")
.Property("health")
.SetValue(100f);- Component() / Component(string): Gets a Unity component from a GameObject. Use when you need to access a component that exists on the object.
- Property(): Navigates to a property or field on the current object. Use when you have a reference and want to access its members.
// If Chassis IS a Rigidbody property on the controller:
Search.Reflect("GameModeDrag.Instance.competitorControllers[1]")
.Property("Chassis") // Chassis is a Rigidbody
.Property("isKinematic") // Access Rigidbody properties
.SetValue(true);
// If you need to get a component from the controller's GameObject:
Search.Reflect("GameModeDrag.Instance.competitorControllers[1]")
.Component<Rigidbody>() // GetComponent<Rigidbody>()
.Property("isKinematic")
.SetValue(true);Create new instances of types for test data using Search.New():
// Create from JSON with generic type
var player = Search.New<PlayerData>(@"{ ""Name"": ""Test"", ""Health"": 100 }");
var data = player.Value as PlayerData;
// Create from JSON by type name (no generic needed)
var player2 = Search.New("PlayerData", @"{ ""Name"": ""Test"", ""Health"": 100 }");
// Create empty instance and set properties
var empty = Search.New("PlayerData");
empty.Property("Name").SetValue("Test");
empty.Property("Health").SetValue(100f);
// Use with SetValue to assign to game state
var newData = Search.New("PlayerData", @"{ ""Name"": ""TestPlayer"" }");
Search.Reflect("GameManager.Instance").Property("PlayerData").SetValue(newData.Value);Serialize and deserialize values to/from JSON using Serialize() and Deserialize() (uses Newtonsoft.Json):
Serialize the current value to a JSON string:
// Get JSON of current state
var json = Search.Reflect("Player.Instance").Property("Stats").Serialize();
// Get formatted/indented JSON
var prettyJson = Search.Reflect("Config.Instance").Property("Settings").Serialize(indented: true);
// Useful for debugging
Debug.Log(Search.Reflect("GameManager.Instance").Property("PlayerData").Serialize(true));
// Save state for later restoration
var savedState = Search.Reflect("LevelManager.Instance").Property("LevelData").Serialize();Deserialize JSON and set it as the property value. The JSON is automatically deserialized to match the property's type:
// Set a complex object from JSON
Search.Reflect("GameManager.Instance")
.Property("PlayerData")
.Deserialize(@"{ ""Name"": ""TestPlayer"", ""Score"": 100, ""Health"": 100.0 }");
// Set an array from JSON
Search.Reflect("Config.Instance")
.Property("Waypoints")
.Deserialize(@"[{""x"":0,""y"":1,""z"":0},{""x"":10,""y"":1,""z"":5}]");
// Restore previously saved state
Search.Reflect("LevelManager.Instance")
.Property("LevelData")
.Deserialize(savedState);
// Set test data for a test scenario
Search.Reflect("Inventory.Instance")
.Property("Items")
.Deserialize(@"[
{ ""Id"": ""sword"", ""Count"": 1 },
{ ""Id"": ""potion"", ""Count"": 5 }
]");// Test setup - initialize game state from JSON
[SetUp]
public void SetUp()
{
Search.Reflect("GameManager.Instance")
.Property("PlayerData")
.Deserialize(@"{ ""Name"": ""Test"", ""Level"": 1, ""XP"": 0 }");
}
// Snapshot and restore pattern
var originalState = Search.Reflect("Config.Instance").Property("Settings").Serialize();
try
{
// Modify settings for test
Search.Reflect("Config.Instance").Property("Settings")
.Deserialize(@"{ ""Difficulty"": ""Hard"", ""Volume"": 0 }");
// Run test...
}
finally
{
// Restore original settings
Search.Reflect("Config.Instance").Property("Settings").Deserialize(originalState);
}Set property or field values via reflection using .SetValue():
// Set property via Property() chain
Search.Reflect("GameManager.Instance")
.Property("Score")
.SetValue(100);
// Set on component accessed via Component()
Search.Reflect("Player.Instance")
.Component<Rigidbody>()
.Property("isKinematic")
.SetValue(true);
// Set on UI element property
new Search().Name("VolumeSlider")
.Property("value")
.SetValue(0.5f);
// Chain multiple Property() calls then SetValue
Search.Reflect("Config.Instance")
.Property("Audio")
.Property("MasterVolume")
.SetValue(0.8f);Note: SetValue() only works on properties accessed via .Property(). It won't work directly on Search.Reflect() results.
using static ODDGames.UIAutomation.ActionExecutor;
[TestFixture]
public class GameStateTest
{
[Test]
public async Task VerifyGameState()
{
// Start game
await Click("StartGame");
// Wait for game to initialize
await WaitFor(Search.Reflect("GameManager.Instance.IsPlaying"));
// Verify initial state
Assert.Greater(Search.Reflect("Player.Health").GetValue<float>(), 0);
Assert.AreEqual(0, Search.Reflect("Player.Score").GetValue<int>());
// Perform game actions
await Click("CollectCoin");
// Verify score increased
Assert.Greater(Search.Reflect("Player.Score").GetValue<int>(), 0);
}
}// Check inventory contents
foreach (var item in Search.Reflect("Inventory.Items"))
{
var itemName = item.Property("Name").GetValue<string>();
var quantity = item.Property("Quantity").GetValue<int>();
Assert.Greater(quantity, 0, $"{itemName} should have positive quantity");
}
// Check specific item
var sword = Search.Reflect("Inventory.Instance")
.Property("EquippedWeapon");
Assert.AreEqual("IronSword", sword.Property("Name").GetValue<string>());
Assert.AreEqual(50, sword.Property("Damage").GetValue<int>());// Verify settings persistence
await Click("Settings");
await Click(Name("SoundToggle")); // Toggle sound off
// Check game state updated
Assert.IsFalse(Search.Reflect("Settings.Instance.SoundEnabled").GetValue<bool>());
// Verify slider value
await DragSlider(Name("VolumeSlider"), 0f, 0.75f);
Assert.AreEqual(0.75f, Search.Reflect("Settings.Instance.Volume").GetValue<float>(), 0.01f);Basic Filters
Proximity
Hierarchy
Spatial
Ordering
Modifiers
Combining
Reflection
- Click Actions
- Text Input
- Drag Actions
- Gesture Input
- Wait and Find
- Assertions
- GameObject Manipulation