test(runner): assert step uses Snapshot, not raw hierarchy/screenshot

TestRunner_UsesAtomicSnapshot catches regressions to the two-goroutine
race, and the existing parallel-fetch test now keys off ActionSnapshot.
This commit is contained in:
pj committed 2026-05-31 15:27:45 +05:30
1 parent 1931c0b57f
commit 5c58610181
1 file changed
+51 -5
+51 -5
View File
@@ -430,19 +430,19 @@ func TestRunner_ParallelFetchCallsAllDriverMethods(t *testing.T) {
} }
actions := state.mock.Actions() actions := state.mock.Actions()
var hasHierarchy, hasMetrics, hasLogs bool var hasSnapshot, hasMetrics, hasLogs bool
for _, a := range actions { for _, a := range actions {
switch a.Kind { switch a.Kind {
case mockdriver.ActionHierarchy: case mockdriver.ActionSnapshot:
hasHierarchy = true hasSnapshot = true
case mockdriver.ActionMetrics: case mockdriver.ActionMetrics:
hasMetrics = true hasMetrics = true
case mockdriver.ActionRecentLogs: case mockdriver.ActionRecentLogs:
hasLogs = true hasLogs = true
} }
} }
if !hasHierarchy { if !hasSnapshot {
t.Error("expected Hierarchy call in mock actions") t.Error("expected Snapshot call in mock actions")
} }
if !hasMetrics { if !hasMetrics {
t.Error("expected Metrics call in mock actions") t.Error("expected Metrics call in mock actions")
@@ -452,6 +452,52 @@ func TestRunner_ParallelFetchCallsAllDriverMethods(t *testing.T) {
} }
} }
// TestRunner_UsesAtomicSnapshot ensures the runner observes a step's UI
// through the paired Snapshot RPC instead of racing two independent
// hierarchy + screenshot reads. The pair must come from one on-device
// frame; a regression to separate calls is what this test catches.
func TestRunner_UsesAtomicSnapshot(t *testing.T) {
state := newHarness(t)
state.mock.ImageData = driver.Image{PNG: []byte("png"), Width: 1, Height: 1}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
summary, err := Run(ctx, Options{
Duration: 100 * time.Millisecond,
IdleTimeout: 20 * time.Millisecond,
Driver: state.mock,
Verifier: state.verifier,
TraceWriter: state.writer,
})
if err != nil {
t.Fatalf("Run: %v", err)
}
if summary.Steps == 0 {
t.Fatal("expected at least one step")
}
var snapshotCalls, hierarchyCalls, screenshotCalls int
for _, action := range state.mock.Actions() {
switch action.Kind {
case mockdriver.ActionSnapshot:
snapshotCalls++
case mockdriver.ActionHierarchy:
hierarchyCalls++
case mockdriver.ActionScreenshot:
screenshotCalls++
}
}
if snapshotCalls == 0 {
t.Errorf("expected at least one Snapshot call, got %d", snapshotCalls)
}
if hierarchyCalls != 0 {
t.Errorf("expected zero standalone Hierarchy calls (runner must use Snapshot), got %d", hierarchyCalls)
}
if screenshotCalls != 0 {
t.Errorf("expected zero standalone Screenshot calls (runner must use Snapshot), got %d", screenshotCalls)
}
}
// TestRunner_OneScreenshotPerStep verifies the runner writes a single // TestRunner_OneScreenshotPerStep verifies the runner writes a single
// screenshot per step, captured concurrently with hierarchy so the two // screenshot per step, captured concurrently with hierarchy so the two
// observations describe the same UI moment. // observations describe the same UI moment.