-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathSampleAppTestHelpers.cs
More file actions
164 lines (146 loc) · 6.69 KB
/
Copy pathSampleAppTestHelpers.cs
File metadata and controls
164 lines (146 loc) · 6.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.UIElements;
namespace Immutable.Audience.Samples.SampleApp.Tests
{
// Test-only utilities for inspecting and driving the sample app's UI.
// Mirrors the LogEntry struct shape from AudienceSample.UI.cs and queries
// the log pane via the userData stash on each log row.
internal static class SampleAppTestHelpers
{
// Reflection field names for AudienceSample's LogEntry.
// No InternalsVisibleTo on the sample-app assembly, so nameof() is unreachable.
private const string LogEntryLabelField = "Label";
private const string LogEntryBodyField = "Body";
private const string LogEntryLevelField = "Level";
// UI Toolkit's Clickable.clicked is non-public; ButtonTestExtensions.Click reflects through this name.
internal const string ClickableClickedField = "clicked";
// Polls predicate once per frame until it returns true or the deadline
// elapses. Calls Assert.Fail with description when the deadline is hit.
// Use this instead of WaitForSecondsRealtime when a test is waiting
// "at most N seconds for X to become true" — the polling exits as soon
// as the condition is satisfied rather than burning the full N seconds.
internal static IEnumerator WaitForCondition(
Func<bool> predicate, float timeoutSeconds, string description)
{
var deadline = Time.realtimeSinceStartup + timeoutSeconds;
while (Time.realtimeSinceStartup < deadline)
{
if (predicate()) yield break;
yield return null;
}
Assert.Fail($"Timed out after {timeoutSeconds:F1}s waiting for: {description}");
}
// Wait until the log pane contains an entry whose label matches `label`
// and whose level matches `level`. Yields one frame per check.
// Throws TimeoutException on deadline.
internal static IEnumerator WaitForLogEntry(
VisualElement root, string label, int level, float timeoutSec)
{
var deadline = Time.realtimeSinceStartup + timeoutSec;
while (Time.realtimeSinceStartup < deadline)
{
if (HasLogEntry(root, label, level))
yield break;
yield return null;
}
throw new TimeoutException(
$"Log entry not found within {timeoutSec}s. " +
$"Looking for label='{label}', level={level}. " +
$"Current entries: {DescribeLogEntries(root)}");
}
internal static bool HasLogEntry(VisualElement root, string label, int level)
{
foreach (var entry in EnumerateLogEntries(root))
{
if (entry.LabelMatches(label) && entry.LevelEquals(level))
return true;
}
return false;
}
internal static int CountLogEntriesAtLevel(VisualElement root, int level)
{
var n = 0;
foreach (var entry in EnumerateLogEntries(root))
if (entry.LevelEquals(level)) n++;
return n;
}
internal static string DescribeLogEntries(VisualElement root)
{
var entries = EnumerateLogEntries(root).ToList();
if (entries.Count == 0) return "(none)";
return string.Join(" | ", entries.Select(e =>
{
var body = e.Body;
return string.IsNullOrEmpty(body) ? $"{e.Label}@{e.Level}" : $"{e.Label}@{e.Level}: {body}";
}));
}
// The log pane stashes an opaque LogEntry on each row's userData.
// Read by reflection so this helper compiles without InternalsVisibleTo.
private static IEnumerable<LogEntryShim> EnumerateLogEntries(VisualElement root)
{
var logView = root.Q<ScrollView>(SampleAppUi.LogScrollView);
if (logView == null) yield break;
// Each direct child of the contentContainer is a log row.
foreach (var row in logView.contentContainer.Children())
{
if (row.userData == null) continue;
yield return new LogEntryShim(row.userData);
}
}
// Adapter over the opaque LogEntry stashed on each row's userData.
// Reads via reflection so this helper compiles without InternalsVisibleTo.
// If the LogEntry struct shape changes, update this adapter.
private readonly struct LogEntryShim
{
private readonly object _entry;
internal LogEntryShim(object entry) { _entry = entry; }
internal string Label =>
(string)(_entry.GetType().GetField(LogEntryLabelField)?.GetValue(_entry) ?? "");
internal string Body =>
(string)(_entry.GetType().GetField(LogEntryBodyField)?.GetValue(_entry) ?? "");
internal int Level
{
get
{
var v = _entry.GetType().GetField(LogEntryLevelField)?.GetValue(_entry);
return v == null ? -1 : Convert.ToInt32(v);
}
}
internal bool LabelMatches(string expected) =>
Label.IndexOf(expected, StringComparison.Ordinal) >= 0;
internal bool LevelEquals(int expected) => Level == expected;
}
}
// Mirrors AudienceSample.UI.cs LogLevel enum: Info=0, Ok=1, Warn=2, Err=3, Debug=4.
// Verified by reading the enum at AudienceSample.UI.cs:675 — matches plan-stated ordering.
internal static class LogLevels
{
internal const int Info = 0;
internal const int Ok = 1;
internal const int Warn = 2;
internal const int Err = 3;
internal const int Debug = 4;
}
// UI Toolkit's Button.clicked event has custom add/remove accessors that
// delegate to Clickable.clicked. The backing delegate lives on the
// Clickable instance, not on Button itself. Reflect on it to invoke
// synchronously without going through the panel's event loop.
internal static class ButtonTestExtensions
{
internal static void Click(this Button button)
{
var clickable = button?.clickable;
if (clickable == null) return;
var field = typeof(Clickable).GetField(SampleAppTestHelpers.ClickableClickedField,
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var handler = field?.GetValue(clickable) as Delegate;
handler?.DynamicInvoke();
}
}
}