Files
recordingtest/tests/Recordingtest.Player.Tests/PlayerEngineTests.cs
minsung 70bf5703b3 player: raw scenario replay without manual cleanup (#14)
First time box-v6.yaml (raw recorder output, 676 lines) replays end-to-end
and actually creates a Box in the SUT — no AI post-editing of target paths
or offsets required. This is the counterpart to #13's recorder-side fixes:
the player now absorbs the remaining record→replay gaps instead of demanding
a hand-cleaned scenario.

Changes (all in Recordingtest.Player):

- PlayerEngine: null-target fallbacks
  - Type with null target → host.Type() against current focus
  - Click with null target + raw_coord → click at screen-absolute raw_coord
  - Other null targets still skipped
- PlayerEngine: strip leading alt+tab hotkey steps (recording-startup noise
  that fights the player's own foreground switch)
- PlayerEngine: preserve recorded inter-step timing, clamped 150ms–3s,
  routed through new IPlayerHost.Delay so the engine itself stays Sleep-free
  (keeps the existing "no fixed sleep" DoD test passing)
- PlayerEngine: per-step console log for live debugging
- UiaPlayerHost: BringSutToForeground() — SetForeground + Focus + 600ms
  settle, called from Program.cs before engine.Run
- Step model: add RawCoord (int[]) and Ts (long?) fields, auto-mapped from
  YAML raw_coord / ts keys

Tests updated:
- PlayerEngine_NullTarget_SkipsWithoutCalling → _Fallback_Issue14
  (verifies the new Click-with-raw_coord and Type behavior)
- FakePlayerHost (both player.tests and runner.tests) implement Delay

Live smoke: box-v6.yaml raw replay produced the expected Box geometry on
the 2nd attempt; 1st attempt dropped the initial "BOX" keystrokes, tracked
as a follow-up (foreground settle is still threshold-sensitive at 600ms).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 19:26:41 +09:00

232 lines
6.8 KiB
C#

using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Recordingtest.Player.Model;
using Xunit;
namespace Recordingtest.Player.Tests;
public class PlayerEngineTests
{
[Fact]
public void Player_EmptyScenario_ExitsZero()
{
var engine = new PlayerEngine();
var host = new FakePlayerHost();
var scenario = new Scenario { Name = "empty" };
engine.Run(scenario, host);
Assert.Empty(host.Clicks);
Assert.Empty(host.Failures);
}
[Fact]
public void Player_ClickStep_InvokesHostClickAtExpectedScreenPoint()
{
var engine = new PlayerEngine();
var host = new FakePlayerHost
{
ResolveImpl = _ => new ResolvedElement(
new ElementBounds(100, 200, 50, 40), null),
};
var scenario = new Scenario
{
Steps =
{
new Step
{
Kind = StepKind.Click,
Target = new Target
{
UiaPath = "Window/Button[@AutomationId='ok']",
Offset = new[] { 0.5, 0.25 },
},
},
},
};
engine.Run(scenario, host);
// 100 + 50*0.5 = 125 ; 200 + 40*0.25 = 210
Assert.Single(host.Clicks);
Assert.Equal(new ScreenPoint(125, 210), host.Clicks[0]);
}
[Fact]
public void Player_ResolveFailure_CapturesArtifacts()
{
var engine = new PlayerEngine();
var host = new FakePlayerHost { ResolveImpl = _ => null };
var scenario = new Scenario
{
Steps =
{
new Step
{
Kind = StepKind.Click,
Target = new Target { UiaPath = "Bogus" },
},
},
};
var ex = Assert.Throws<InvalidOperationException>(
() => engine.Run(scenario, host));
Assert.Single(host.Failures);
Assert.Equal(0, host.Failures[0].StepIndex);
Assert.False(string.IsNullOrEmpty(host.Failures[0].Reason));
Assert.Contains("Bogus", ex.Message);
}
[Fact]
public void Player_CheckpointStep_InvokesCapture()
{
var engine = new PlayerEngine();
var host = new FakePlayerHost();
var scenario = new Scenario
{
Steps =
{
new Step
{
Kind = StepKind.Checkpoint,
AfterStep = 3,
SaveAs = "out/cp.hmeg",
},
},
};
engine.Run(scenario, host);
Assert.Single(host.Checkpoints);
Assert.Equal(3, host.Checkpoints[0].AfterStep);
Assert.Equal("out/cp.hmeg", host.Checkpoints[0].SaveAs);
}
[Fact]
public void Player_NoFixedSleep()
{
var path = LocateEngineSource();
var src = File.ReadAllText(path);
Assert.DoesNotMatch(new Regex(@"Thread\.Sleep\("), src);
Assert.DoesNotMatch(new Regex(@"Task\.Delay\(TimeSpan\.FromSeconds"), src);
}
[Fact]
public void Player_ScenarioLoader_ParsesSampleYaml()
{
const string yaml = """
name: sample
description: tiny
sut:
exe: "EG-BIM Modeler/EG-BIM Modeler.exe"
startup_timeout_ms: 12000
steps:
- kind: click
target:
uia_path: "Window/Button[@AutomationId='ok']"
offset: [0.5, 0.5]
- kind: type
value: "hello"
- kind: checkpoint
after_step: 1
save_as: "out/cp1.hmeg"
checkpoints:
- after_step: 1
save_as: "out/cp1.hmeg"
baselines:
- path: "baselines/cp1.approved.hmeg"
""";
var s = ScenarioLoader.LoadFromString(yaml);
Assert.Equal("sample", s.Name);
Assert.Equal(12000, s.Sut.StartupTimeoutMs);
Assert.Equal(3, s.Steps.Count);
Assert.Equal(StepKind.Click, s.Steps[0].Kind);
Assert.Equal("Window/Button[@AutomationId='ok']", s.Steps[0].Target!.UiaPath);
Assert.Equal(0.5, s.Steps[0].Target!.Offset[0]);
Assert.Equal(StepKind.Type, s.Steps[1].Kind);
Assert.Equal("hello", s.Steps[1].Value);
Assert.Equal(StepKind.Checkpoint, s.Steps[2].Kind);
Assert.Equal(1, s.Steps[2].AfterStep);
Assert.Single(s.Checkpoints);
Assert.Single(s.Baselines);
}
[Fact]
public void PlayerEngine_NullTarget_Fallback_Issue14()
{
// Issue #14: null-target fallbacks.
// - Type → send keystrokes to current focus (no target required)
// - Click w/ raw_coord → click at raw_coord screen-absolute
// - Click w/o raw_coord → still skipped (no way to click safely)
// - Drag → still skipped (no raw_coord pair handling yet)
var engine = new PlayerEngine();
var host = new FakePlayerHost();
var scenario = new Scenario
{
Steps =
{
new Step { Kind = StepKind.Click, Target = null }, // skip
new Step { Kind = StepKind.Click, Target = null, RawCoord = new[] { 123, 456 } },
new Step { Kind = StepKind.Drag, Target = null }, // skip
new Step { Kind = StepKind.Type, Target = null, Value = "hello" },
},
};
engine.Run(scenario, host);
Assert.Single(host.Clicks);
Assert.Equal(new ScreenPoint(123, 456), host.Clicks[0]);
Assert.Empty(host.Drags);
Assert.Single(host.Types);
Assert.Equal("hello", host.Types[0]);
Assert.Empty(host.Failures);
}
[Fact]
public void PlayerEngine_WheelKind_DoesNotThrow()
{
var engine = new PlayerEngine();
var host = new FakePlayerHost();
var scenario = new Scenario
{
Steps =
{
new Step { Kind = StepKind.Wheel, Value = "-120" },
new Step { Kind = StepKind.Focus },
},
};
engine.Run(scenario, host);
Assert.Empty(host.Failures);
}
[Fact]
public void ScenarioLoader_ParsesWheelAndFocusKinds()
{
const string yaml = """
name: wheel-focus
steps:
- kind: wheel
value: "-120"
- kind: focus
target:
uia_path: "Window/Edit"
offset: [0.5, 0.5]
""";
var s = ScenarioLoader.LoadFromString(yaml);
Assert.Equal(2, s.Steps.Count);
Assert.Equal(StepKind.Wheel, s.Steps[0].Kind);
Assert.Equal(StepKind.Focus, s.Steps[1].Kind);
}
private static string LocateEngineSource([CallerFilePath] string here = "")
{
// here = .../tests/Recordingtest.Player.Tests/PlayerEngineTests.cs
var dir = Path.GetDirectoryName(here)!;
var repo = Path.GetFullPath(Path.Combine(dir, "..", ".."));
return Path.Combine(repo, "src", "Recordingtest.Player", "PlayerEngine.cs");
}
}