@mflrevan/ucp 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bridge/com.ucp.bridge/CHANGELOG.md +48 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +55 -6
- package/bridge/com.ucp.bridge/Editor/Bridge/EditorStateSummary.cs +165 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/EditorStateSummary.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +22 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorController.cs +1 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +10 -10
- package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +71 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +4 -4
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +6 -6
- package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +7 -7
- package/bridge/com.ucp.bridge/Editor/Controllers/RecordingController.cs +8 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +5 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/UiController.cs +70 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/UiController.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Ui/UiHost.cs +468 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiHost.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiLintService.cs +554 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiLintService.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiOperationManager.cs +870 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiOperationManager.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioApplier.cs +496 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioApplier.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioLoader.cs +854 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioLoader.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioModels.cs +374 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioModels.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiVisualTreeInspector.cs +690 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiVisualTreeInspector.cs.meta +11 -0
- package/bridge/com.ucp.bridge/{Runtime.meta → Editor/Ui.meta} +1 -1
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +79 -4
- package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +2 -2
- package/bridge/com.ucp.bridge/Tests/Editor/UiControllerTests.cs +547 -0
- package/bridge/com.ucp.bridge/Tests/Editor/UiControllerTests.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Tests/Editor/UiLintServiceTests.cs +253 -0
- package/bridge/com.ucp.bridge/Tests/Editor/UiLintServiceTests.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Tests/Editor/UiScenarioTests.cs +587 -0
- package/bridge/com.ucp.bridge/Tests/Editor/UiScenarioTests.cs.meta +11 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- package/package.json +1 -1
- package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef +0 -14
- package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef.meta +0 -7
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
#if UNITY_6000_0_OR_NEWER
|
|
2
|
+
using System;
|
|
3
|
+
using System.Collections;
|
|
4
|
+
using System.Collections.Generic;
|
|
5
|
+
using System.IO;
|
|
6
|
+
using System.Linq;
|
|
7
|
+
using NUnit.Framework;
|
|
8
|
+
using UnityEditor;
|
|
9
|
+
using UnityEngine;
|
|
10
|
+
using UnityEngine.TestTools;
|
|
11
|
+
using UnityEngine.UIElements;
|
|
12
|
+
|
|
13
|
+
namespace UCP.Bridge.Tests
|
|
14
|
+
{
|
|
15
|
+
public sealed class UiScenarioTests
|
|
16
|
+
{
|
|
17
|
+
private const string TestFolder = "Assets/UcpUiScenarioTests";
|
|
18
|
+
private const string DocumentPath = TestFolder + "/Document.uxml";
|
|
19
|
+
private const string RowPath = TestFolder + "/Row.uxml";
|
|
20
|
+
|
|
21
|
+
[SetUp]
|
|
22
|
+
public void SetUp()
|
|
23
|
+
{
|
|
24
|
+
DeleteTestFolder();
|
|
25
|
+
Directory.CreateDirectory(AbsolutePath(TestFolder));
|
|
26
|
+
File.WriteAllText(
|
|
27
|
+
AbsolutePath(DocumentPath),
|
|
28
|
+
"<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:VisualElement name=\"document-root\" /></ui:UXML>");
|
|
29
|
+
File.WriteAllText(
|
|
30
|
+
AbsolutePath(RowPath),
|
|
31
|
+
"<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:VisualElement name=\"row-root\"><ui:Label name=\"row-label\" text=\"pending\"><Bindings><ui:DataBinding property=\"text\" data-source-path=\"name\" binding-mode=\"ToTarget\" /></Bindings></ui:Label></ui:VisualElement></ui:UXML>");
|
|
32
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[TearDown]
|
|
36
|
+
public void TearDown()
|
|
37
|
+
{
|
|
38
|
+
DeleteTestFolder();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[Test]
|
|
42
|
+
public void Resolve_RejectsUnknownFixtureFields()
|
|
43
|
+
{
|
|
44
|
+
var fixturePath = WriteFixture(
|
|
45
|
+
"Unknown.ucp-ui.json",
|
|
46
|
+
"{\"schemaVersion\":0,\"document\":\"./Document.uxml\",\"unexpected\":true,\"states\":{\"default\":{}}}");
|
|
47
|
+
|
|
48
|
+
var exception = Assert.Throws<UiScenarioException>(() => UiScenarioLoader.Resolve(
|
|
49
|
+
new Dictionary<string, object> { ["target"] = fixturePath }));
|
|
50
|
+
|
|
51
|
+
Assert.That(exception.Code, Is.EqualTo("ui.unknown-field"));
|
|
52
|
+
Assert.That(exception.Location, Does.EndWith(".unexpected"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[TestCase("Packages/com.example.ui/Styles/../Theme.tss", null, "Packages/com.example.ui/Theme.tss")]
|
|
56
|
+
[TestCase("./Theme.tss", "Packages/com.example.ui/Panel.ucp-ui.json", "Packages/com.example.ui/Theme.tss")]
|
|
57
|
+
[TestCase("Assets/UI/../Panel.uxml", null, "Assets/Panel.uxml")]
|
|
58
|
+
public void NormalizeAssetPath_PreservesVirtualRoots(string path, string fixture, string expected)
|
|
59
|
+
{
|
|
60
|
+
Assert.That(UiScenarioLoader.NormalizeAssetPath(path, fixture, "$test"), Is.EqualTo(expected));
|
|
61
|
+
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
|
62
|
+
Assert.That(UiScenarioLoader.NormalizeAssetPath(Path.Combine(projectRoot, expected), null, "$test"),
|
|
63
|
+
Is.EqualTo(expected));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
[TestCase("Packages/../../outside.uss")]
|
|
67
|
+
[TestCase("Assets/../Library/Theme.tss")]
|
|
68
|
+
[TestCase("Packages/com.example/../../Library/Theme.tss")]
|
|
69
|
+
public void NormalizeAssetPath_RejectsEscapesAfterCollapsingSegments(string path)
|
|
70
|
+
{
|
|
71
|
+
Assert.Throws<UiScenarioException>(() => UiScenarioLoader.NormalizeAssetPath(path, null, "$test"));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
[Test]
|
|
75
|
+
public void Resolve_RejectsMoreThanSixtyFourStates()
|
|
76
|
+
{
|
|
77
|
+
var states = new Dictionary<string, object>();
|
|
78
|
+
for (var index = 0; index < 65; index++)
|
|
79
|
+
states["state-" + index] = new Dictionary<string, object>();
|
|
80
|
+
|
|
81
|
+
var fixturePath = WriteFixture(
|
|
82
|
+
"TooManyStates.ucp-ui.json",
|
|
83
|
+
MiniJson.Serialize(new Dictionary<string, object>
|
|
84
|
+
{
|
|
85
|
+
["schemaVersion"] = 0,
|
|
86
|
+
["document"] = "./Document.uxml",
|
|
87
|
+
["defaultState"] = "state-0",
|
|
88
|
+
["states"] = states
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
var exception = Assert.Throws<UiScenarioException>(() => UiScenarioLoader.Resolve(
|
|
92
|
+
new Dictionary<string, object> { ["target"] = fixturePath },
|
|
93
|
+
allStates: true));
|
|
94
|
+
|
|
95
|
+
Assert.That(exception.Code, Is.EqualTo("ui.states-limit"));
|
|
96
|
+
Assert.That(exception.Location, Does.EndWith("#states"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[Test]
|
|
100
|
+
public void Resolve_RejectsViewportAreaForFixtureAndPartialOverride()
|
|
101
|
+
{
|
|
102
|
+
var oversizedPath = WriteFixture(
|
|
103
|
+
"OversizedViewport.ucp-ui.json",
|
|
104
|
+
"{\"schemaVersion\":0,\"document\":\"./Document.uxml\",\"viewport\":{\"width\":4096,\"height\":2049},\"states\":{\"default\":{}}}");
|
|
105
|
+
|
|
106
|
+
var fixtureException = Assert.Throws<UiScenarioException>(() => UiScenarioLoader.Resolve(
|
|
107
|
+
new Dictionary<string, object> { ["target"] = oversizedPath }));
|
|
108
|
+
Assert.That(fixtureException.Code, Is.EqualTo("ui.viewport-area"));
|
|
109
|
+
Assert.That(fixtureException.Location, Does.EndWith("#viewport"));
|
|
110
|
+
|
|
111
|
+
var boundaryPath = WriteFixture(
|
|
112
|
+
"BoundaryViewport.ucp-ui.json",
|
|
113
|
+
"{\"schemaVersion\":0,\"document\":\"./Document.uxml\",\"viewport\":{\"width\":4096,\"height\":2048},\"states\":{\"default\":{}}}");
|
|
114
|
+
Assert.That(UiScenarioLoader.Resolve(
|
|
115
|
+
new Dictionary<string, object> { ["target"] = boundaryPath })[0].Viewport.Height,
|
|
116
|
+
Is.EqualTo(2048));
|
|
117
|
+
|
|
118
|
+
var requestException = Assert.Throws<UiScenarioException>(() => UiScenarioLoader.Resolve(
|
|
119
|
+
new Dictionary<string, object>
|
|
120
|
+
{
|
|
121
|
+
["target"] = boundaryPath,
|
|
122
|
+
["viewport"] = new Dictionary<string, object> { ["height"] = 2049L }
|
|
123
|
+
}));
|
|
124
|
+
Assert.That(requestException.Code, Is.EqualTo("ui.viewport-area"));
|
|
125
|
+
Assert.That(requestException.Location, Is.EqualTo("$request.viewport"));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
[Test]
|
|
129
|
+
public void Resolve_SelectsDefaultAndMergesNestedRequestData()
|
|
130
|
+
{
|
|
131
|
+
var fixturePath = WriteFixture(
|
|
132
|
+
"States.ucp-ui.json",
|
|
133
|
+
"{"
|
|
134
|
+
+ "\"schemaVersion\":0,"
|
|
135
|
+
+ "\"document\":\"./Document.uxml\","
|
|
136
|
+
+ "\"defaultState\":\"populated\","
|
|
137
|
+
+ "\"data\":{\"nested\":{\"shared\":\"base\",\"count\":1}},"
|
|
138
|
+
+ "\"states\":{"
|
|
139
|
+
+ "\"empty\":{\"data\":{\"items\":[]}},"
|
|
140
|
+
+ "\"populated\":{\"viewport\":{\"width\":800,\"height\":500},\"data\":{\"nested\":{\"count\":2},\"items\":[{\"name\":\"One\"}]}}"
|
|
141
|
+
+ "}}");
|
|
142
|
+
|
|
143
|
+
var resolved = UiScenarioLoader.Resolve(new Dictionary<string, object>
|
|
144
|
+
{
|
|
145
|
+
["target"] = fixturePath,
|
|
146
|
+
["data"] = new Dictionary<string, object>
|
|
147
|
+
{
|
|
148
|
+
["nested"] = new Dictionary<string, object> { ["count"] = 9L }
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
Assert.That(resolved, Has.Count.EqualTo(1));
|
|
153
|
+
Assert.That(resolved[0].StateName, Is.EqualTo("populated"));
|
|
154
|
+
Assert.That(resolved[0].DocumentPath, Is.EqualTo(DocumentPath));
|
|
155
|
+
Assert.That(resolved[0].Viewport.Width, Is.EqualTo(800));
|
|
156
|
+
Assert.That(resolved[0].Viewport.Height, Is.EqualTo(500));
|
|
157
|
+
var nested = (Dictionary<string, object>)resolved[0].Data["nested"];
|
|
158
|
+
Assert.That(nested["shared"], Is.EqualTo("base"));
|
|
159
|
+
Assert.That(nested["count"], Is.TypeOf<int>().And.EqualTo(9));
|
|
160
|
+
|
|
161
|
+
var all = UiScenarioLoader.Resolve(
|
|
162
|
+
new Dictionary<string, object> { ["target"] = fixturePath },
|
|
163
|
+
allStates: true);
|
|
164
|
+
Assert.That(all.Select(scenario => scenario.StateName), Is.EqualTo(new[] { "empty", "populated" }));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
[Test]
|
|
168
|
+
public void Resolve_NormalizesOnlySafeIntegersToInt32()
|
|
169
|
+
{
|
|
170
|
+
var fixturePath = WriteFixture(
|
|
171
|
+
"Numbers.ucp-ui.json",
|
|
172
|
+
"{\"schemaVersion\":0,\"document\":\"./Document.uxml\",\"data\":{\"small\":42,\"large\":2147483648,\"items\":[-1,2147483647]},\"states\":{\"default\":{}}}");
|
|
173
|
+
|
|
174
|
+
var scenario = UiScenarioLoader.Resolve(
|
|
175
|
+
new Dictionary<string, object> { ["target"] = fixturePath })[0];
|
|
176
|
+
var items = (List<object>)scenario.Data["items"];
|
|
177
|
+
|
|
178
|
+
Assert.That(scenario.Data["small"], Is.TypeOf<int>().And.EqualTo(42));
|
|
179
|
+
Assert.That(scenario.Data["large"], Is.TypeOf<long>().And.EqualTo(2147483648L));
|
|
180
|
+
Assert.That(items[0], Is.TypeOf<int>().And.EqualTo(-1));
|
|
181
|
+
Assert.That(items[1], Is.TypeOf<int>().And.EqualTo(int.MaxValue));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
[Test]
|
|
185
|
+
public void Apply_BuildsRepeatAndOwnsListViewBindingLifecycle()
|
|
186
|
+
{
|
|
187
|
+
var document = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(DocumentPath);
|
|
188
|
+
var rowTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(RowPath);
|
|
189
|
+
Assert.That(document, Is.Not.Null);
|
|
190
|
+
Assert.That(rowTemplate, Is.Not.Null);
|
|
191
|
+
|
|
192
|
+
var items = new List<object>
|
|
193
|
+
{
|
|
194
|
+
Item("One"),
|
|
195
|
+
Item("Two"),
|
|
196
|
+
Item("Three")
|
|
197
|
+
};
|
|
198
|
+
var data = new Dictionary<string, object> { ["items"] = items };
|
|
199
|
+
var collections = new List<UiCollectionDefinition>
|
|
200
|
+
{
|
|
201
|
+
new UiCollectionDefinition("#repeat", UiCollectionMode.Repeat, "/items", RowPath, rowTemplate, null, "$test.repeat"),
|
|
202
|
+
new UiCollectionDefinition("#list", UiCollectionMode.ListView, "/items", RowPath, rowTemplate, 24f, "$test.list")
|
|
203
|
+
};
|
|
204
|
+
var scenario = new UiResolvedScenario(
|
|
205
|
+
DocumentPath,
|
|
206
|
+
null,
|
|
207
|
+
"default",
|
|
208
|
+
DocumentPath,
|
|
209
|
+
document,
|
|
210
|
+
data,
|
|
211
|
+
Array.Empty<UiSetOperation>(),
|
|
212
|
+
collections,
|
|
213
|
+
new UiViewport(),
|
|
214
|
+
new UiSettleOptions());
|
|
215
|
+
|
|
216
|
+
var root = new VisualElement();
|
|
217
|
+
var repeat = new VisualElement { name = "repeat" };
|
|
218
|
+
var list = new ListView { name = "list" };
|
|
219
|
+
root.Add(repeat);
|
|
220
|
+
root.Add(list);
|
|
221
|
+
|
|
222
|
+
var report = UiScenarioApplier.Apply(root, scenario);
|
|
223
|
+
|
|
224
|
+
Assert.That(repeat.childCount, Is.EqualTo(3));
|
|
225
|
+
Assert.That(report.RepeatItemCount, Is.EqualTo(3));
|
|
226
|
+
Assert.That(list.itemsSource, Is.SameAs(items));
|
|
227
|
+
Assert.That(list.makeItem, Is.Not.Null);
|
|
228
|
+
Assert.That(list.bindItem, Is.Not.Null);
|
|
229
|
+
Assert.That(list.unbindItem, Is.Not.Null);
|
|
230
|
+
Assert.That(list.destroyItem, Is.Not.Null);
|
|
231
|
+
Assert.That(list.virtualizationMethod, Is.EqualTo(CollectionVirtualizationMethod.FixedHeight));
|
|
232
|
+
Assert.That(list.fixedItemHeight, Is.EqualTo(24f));
|
|
233
|
+
|
|
234
|
+
var row = list.makeItem();
|
|
235
|
+
list.bindItem(row, 1);
|
|
236
|
+
Assert.That(row.dataSource, Is.SameAs(items[1]));
|
|
237
|
+
|
|
238
|
+
var metadata = UiScenarioApplier.GetCollectionMetadata(root);
|
|
239
|
+
Assert.That(metadata, Has.Count.EqualTo(2));
|
|
240
|
+
var repeatMetadata = metadata.Single(item => item.Mode == UiCollectionMode.Repeat);
|
|
241
|
+
var listMetadata = metadata.Single(item => item.Mode == UiCollectionMode.ListView);
|
|
242
|
+
Assert.That(repeatMetadata.LogicalItemCount, Is.EqualTo(3));
|
|
243
|
+
Assert.That(repeatMetadata.RealizedRowCount, Is.EqualTo(3));
|
|
244
|
+
Assert.That(repeatMetadata.BoundRowCount, Is.EqualTo(3));
|
|
245
|
+
Assert.That(listMetadata.LogicalItemCount, Is.EqualTo(3));
|
|
246
|
+
Assert.That(listMetadata.RealizedRowCount, Is.EqualTo(1));
|
|
247
|
+
Assert.That(listMetadata.BoundRowCount, Is.EqualTo(1));
|
|
248
|
+
|
|
249
|
+
list.bindItem(row, 2);
|
|
250
|
+
Assert.That(listMetadata.RealizedRowCount, Is.EqualTo(1));
|
|
251
|
+
Assert.That(listMetadata.BoundRowCount, Is.EqualTo(1));
|
|
252
|
+
var boundRows = (List<object>)listMetadata.ToDictionary()["boundRows"];
|
|
253
|
+
Assert.That(((Dictionary<string, object>)boundRows[0])["index"], Is.EqualTo(2));
|
|
254
|
+
|
|
255
|
+
list.unbindItem(row, 2);
|
|
256
|
+
Assert.That(row.dataSource, Is.Null);
|
|
257
|
+
Assert.That(listMetadata.BoundRowCount, Is.Zero);
|
|
258
|
+
|
|
259
|
+
list.bindItem(row, 0);
|
|
260
|
+
Assert.That(listMetadata.BoundRowCount, Is.EqualTo(1));
|
|
261
|
+
list.destroyItem(row);
|
|
262
|
+
Assert.That(row.dataSource, Is.Null);
|
|
263
|
+
Assert.That(listMetadata.RealizedRowCount, Is.Zero);
|
|
264
|
+
Assert.That(listMetadata.BoundRowCount, Is.Zero);
|
|
265
|
+
Assert.That(report.BindingRewriteCount, Is.GreaterThanOrEqualTo(4));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
[Test]
|
|
269
|
+
public void Apply_RejectsAmbiguousSelectors()
|
|
270
|
+
{
|
|
271
|
+
var document = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(DocumentPath);
|
|
272
|
+
var root = new VisualElement();
|
|
273
|
+
var first = new VisualElement();
|
|
274
|
+
var second = new VisualElement();
|
|
275
|
+
first.AddToClassList("duplicate");
|
|
276
|
+
second.AddToClassList("duplicate");
|
|
277
|
+
root.Add(first);
|
|
278
|
+
root.Add(second);
|
|
279
|
+
|
|
280
|
+
var scenario = new UiResolvedScenario(
|
|
281
|
+
DocumentPath,
|
|
282
|
+
null,
|
|
283
|
+
"default",
|
|
284
|
+
DocumentPath,
|
|
285
|
+
document,
|
|
286
|
+
new Dictionary<string, object>(),
|
|
287
|
+
new[] { new UiSetOperation(".duplicate", "enabled", true, "$test.set") },
|
|
288
|
+
Array.Empty<UiCollectionDefinition>(),
|
|
289
|
+
new UiViewport(),
|
|
290
|
+
new UiSettleOptions());
|
|
291
|
+
|
|
292
|
+
var exception = Assert.Throws<UiScenarioException>(() => UiScenarioApplier.Apply(root, scenario));
|
|
293
|
+
Assert.That(exception.Code, Is.EqualTo("ui.selector-ambiguous"));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
[UnityTest]
|
|
297
|
+
public IEnumerator InspectAndAudit_IncludeRealizedListViewRowsAndBindingFailures()
|
|
298
|
+
{
|
|
299
|
+
if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null)
|
|
300
|
+
Assert.Ignore("Realized ListView binding coverage requires an Editor graphics device");
|
|
301
|
+
|
|
302
|
+
var previousFocus = EditorWindow.focusedWindow;
|
|
303
|
+
var window = UiHostWindow.Open(320, 240);
|
|
304
|
+
try
|
|
305
|
+
{
|
|
306
|
+
var root = window.ContentRoot;
|
|
307
|
+
Binding.SetPanelLogLevel(root.panel, BindingLogLevel.None);
|
|
308
|
+
var list = new ListView { name = "rows" };
|
|
309
|
+
list.style.height = 160;
|
|
310
|
+
root.Add(list);
|
|
311
|
+
var scenario = new UiResolvedScenario(
|
|
312
|
+
DocumentPath, null, "default", DocumentPath,
|
|
313
|
+
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(DocumentPath),
|
|
314
|
+
new Dictionary<string, object>
|
|
315
|
+
{
|
|
316
|
+
["items"] = new List<object> { Item("One"), new Dictionary<string, object>() }
|
|
317
|
+
},
|
|
318
|
+
Array.Empty<UiSetOperation>(),
|
|
319
|
+
new[]
|
|
320
|
+
{
|
|
321
|
+
new UiCollectionDefinition("#rows", UiCollectionMode.ListView, "/items", RowPath,
|
|
322
|
+
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(RowPath), 24f, "$test.list")
|
|
323
|
+
},
|
|
324
|
+
new UiViewport(320, 240), new UiSettleOptions());
|
|
325
|
+
var report = UiScenarioApplier.Apply(root, scenario);
|
|
326
|
+
var metadata = UiScenarioApplier.GetCollectionMetadata(root);
|
|
327
|
+
List<Label> labels = null;
|
|
328
|
+
var deadline = EditorApplication.timeSinceStartup + 10;
|
|
329
|
+
while (EditorApplication.timeSinceStartup < deadline)
|
|
330
|
+
{
|
|
331
|
+
window.Repaint();
|
|
332
|
+
EditorApplication.QueuePlayerLoopUpdate();
|
|
333
|
+
yield return null;
|
|
334
|
+
labels = root.Query<Label>("row-label").ToList();
|
|
335
|
+
if (labels.Any(label => label.text == "One") && labels.Any(label =>
|
|
336
|
+
label.TryGetLastBindingToUIResult("text", out var result) &&
|
|
337
|
+
result.status == BindingStatus.Failure))
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
Assert.That(labels, Has.Count.EqualTo(2), "Both visible rows should be realized");
|
|
341
|
+
Assert.That(labels.Any(label => label.text == "One"), Is.True, "Valid row binding should resolve");
|
|
342
|
+
Assert.That(labels.Any(label =>
|
|
343
|
+
label.TryGetLastBindingToUIResult("text", out var result) &&
|
|
344
|
+
result.status == BindingStatus.Failure), Is.True, "Missing row data should fail binding");
|
|
345
|
+
|
|
346
|
+
var snapshot = UiVisualTreeInspector.Inspect(root,
|
|
347
|
+
UiInspectOptions.Parse(new Dictionary<string, object> { ["query"] = "#row-label" }), metadata);
|
|
348
|
+
Assert.That(snapshot["matchCount"], Is.EqualTo(2));
|
|
349
|
+
Assert.That(snapshot["returnedElementCount"], Is.EqualTo(2));
|
|
350
|
+
var fullSnapshot = UiVisualTreeInspector.Inspect(list,
|
|
351
|
+
UiInspectOptions.Parse(new Dictionary<string, object> { ["depth"] = 64 }), metadata);
|
|
352
|
+
Assert.That(MiniJson.Serialize(fullSnapshot), Does.Contain("row-label"));
|
|
353
|
+
var boundedSnapshot = UiVisualTreeInspector.Inspect(list,
|
|
354
|
+
UiInspectOptions.Parse(new Dictionary<string, object> { ["depth"] = 64, ["max"] = 2 }), metadata);
|
|
355
|
+
Assert.That(boundedSnapshot["returnedElementCount"], Is.EqualTo(2));
|
|
356
|
+
Assert.That(boundedSnapshot["truncated"], Is.True);
|
|
357
|
+
|
|
358
|
+
var before = UiGeometrySampler.Measure(root);
|
|
359
|
+
labels[0].text = "Changed row";
|
|
360
|
+
Assert.That(UiGeometrySampler.Measure(root).Hash, Is.Not.EqualTo(before.Hash),
|
|
361
|
+
"Row changes must invalidate geometry stability");
|
|
362
|
+
|
|
363
|
+
var audit = UiAudit.Run(root, report, metadata);
|
|
364
|
+
Assert.That(audit["passed"], Is.False);
|
|
365
|
+
var errors = (List<object>)audit["errors"];
|
|
366
|
+
Assert.That(errors.OfType<Dictionary<string, object>>().Any(error =>
|
|
367
|
+
Equals(error["code"], "binding_failure") &&
|
|
368
|
+
Equals(((Dictionary<string, object>)error["element"])["name"], "row-label")), Is.True);
|
|
369
|
+
Assert.That(((List<object>)audit["warnings"]).OfType<Dictionary<string, object>>().Any(warning =>
|
|
370
|
+
Equals(warning["code"], "duplicate_name")), Is.False,
|
|
371
|
+
"Repeated names in managed rows must still be exempt from duplicate-name warnings");
|
|
372
|
+
|
|
373
|
+
for (var index = 0; index < 200; index++)
|
|
374
|
+
report.AddDiagnostic("warning", "test", "warning before binding audit");
|
|
375
|
+
var truncatedAudit = UiAudit.Run(root, report, metadata);
|
|
376
|
+
Assert.That(truncatedAudit["passed"], Is.False);
|
|
377
|
+
Assert.That(truncatedAudit["errorCount"], Is.EqualTo(audit["errorCount"]));
|
|
378
|
+
Assert.That((List<object>)truncatedAudit["warnings"],
|
|
379
|
+
Has.Count.EqualTo(200 - ((List<object>)audit["errors"]).Count));
|
|
380
|
+
Assert.That(MiniJson.Serialize(truncatedAudit["errors"]),
|
|
381
|
+
Is.EqualTo(MiniJson.Serialize(audit["errors"])));
|
|
382
|
+
Assert.That(truncatedAudit["diagnosticsTruncated"], Is.True);
|
|
383
|
+
}
|
|
384
|
+
finally
|
|
385
|
+
{
|
|
386
|
+
if (window != null && window.rootVisualElement.panel != null)
|
|
387
|
+
Binding.ResetPanelLogLevel(window.rootVisualElement.panel);
|
|
388
|
+
UiHostWindow.CloseAndDestroy(window);
|
|
389
|
+
if (previousFocus != null)
|
|
390
|
+
previousFocus.Focus();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
[UnityTest]
|
|
395
|
+
public IEnumerator Audit_VisibleEmptyListViewSkipsInternalFocusTargetsButChecksAuthoredControls()
|
|
396
|
+
{
|
|
397
|
+
if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null)
|
|
398
|
+
Assert.Ignore("Empty ListView layout coverage requires an Editor graphics device");
|
|
399
|
+
|
|
400
|
+
var previousFocus = EditorWindow.focusedWindow;
|
|
401
|
+
var window = UiHostWindow.Open(320, 240);
|
|
402
|
+
try
|
|
403
|
+
{
|
|
404
|
+
var root = window.ContentRoot;
|
|
405
|
+
var list = new ListView { name = "rows", itemsSource = new List<object>() };
|
|
406
|
+
list.style.height = 160;
|
|
407
|
+
root.Add(list);
|
|
408
|
+
var deadline = EditorApplication.timeSinceStartup + 10;
|
|
409
|
+
var stableFrames = 0;
|
|
410
|
+
while (stableFrames < 3 && EditorApplication.timeSinceStartup < deadline)
|
|
411
|
+
{
|
|
412
|
+
window.Pump();
|
|
413
|
+
yield return null;
|
|
414
|
+
stableFrames = list.worldBound.height > 0 && list.worldBound.width > 0
|
|
415
|
+
? stableFrames + 1 : 0;
|
|
416
|
+
}
|
|
417
|
+
Assert.That(stableFrames, Is.EqualTo(3), "The empty list must be displayed and laid out");
|
|
418
|
+
var audit = UiAudit.Run(root, new UiApplyReport(), Array.Empty<UiCollectionMetadata>());
|
|
419
|
+
Assert.That(audit["errorCount"], Is.EqualTo(0));
|
|
420
|
+
Assert.That(audit["warningCount"], Is.EqualTo(0), MiniJson.Serialize(audit));
|
|
421
|
+
|
|
422
|
+
// An unnamed authored control must not be exempted with the internals.
|
|
423
|
+
var authored = new VisualElement { focusable = true };
|
|
424
|
+
authored.style.width = 0;
|
|
425
|
+
authored.style.height = 0;
|
|
426
|
+
root.Add(authored);
|
|
427
|
+
for (var frame = 0; frame < 3; frame++)
|
|
428
|
+
{
|
|
429
|
+
window.Pump();
|
|
430
|
+
yield return null;
|
|
431
|
+
}
|
|
432
|
+
audit = UiAudit.Run(root, new UiApplyReport(), Array.Empty<UiCollectionMetadata>());
|
|
433
|
+
Assert.That(audit["warningCount"], Is.EqualTo(1));
|
|
434
|
+
Assert.That(MiniJson.Serialize(audit["warnings"]), Does.Contain("focusable_zero_size"));
|
|
435
|
+
}
|
|
436
|
+
finally
|
|
437
|
+
{
|
|
438
|
+
UiHostWindow.CloseAndDestroy(window);
|
|
439
|
+
if (previousFocus != null)
|
|
440
|
+
previousFocus.Focus();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
[TestCase(":root")]
|
|
445
|
+
[TestCase("#row-label")]
|
|
446
|
+
[TestCase(".card_item-2")]
|
|
447
|
+
public void ValidateSelector_AcceptsRootNameAndClassSelectors(string selector)
|
|
448
|
+
{
|
|
449
|
+
Assert.DoesNotThrow(() => UiScenarioLoader.ValidateSelector(selector, "$test"));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
[TestCase("row-label")]
|
|
453
|
+
[TestCase("#")]
|
|
454
|
+
[TestCase("#a b")]
|
|
455
|
+
[TestCase(".a>b")]
|
|
456
|
+
[TestCase("#a.b")]
|
|
457
|
+
public void ValidateSelector_RejectsAnythingButOneSimpleSelector(string selector)
|
|
458
|
+
{
|
|
459
|
+
var exception = Assert.Throws<UiScenarioException>(
|
|
460
|
+
() => UiScenarioLoader.ValidateSelector(selector, "$test"));
|
|
461
|
+
Assert.That(exception.Code, Is.EqualTo("ui.selector-syntax"));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
[Test]
|
|
465
|
+
public void Apply_SetOperations_CoverEveryAllowlistedProperty()
|
|
466
|
+
{
|
|
467
|
+
var root = new VisualElement();
|
|
468
|
+
var label = new Label("before") { name = "label" };
|
|
469
|
+
var field = new TextField { name = "field" };
|
|
470
|
+
var toggle = new Toggle { name = "toggle" };
|
|
471
|
+
var panel = new VisualElement { name = "panel" };
|
|
472
|
+
root.Add(label);
|
|
473
|
+
root.Add(field);
|
|
474
|
+
root.Add(toggle);
|
|
475
|
+
root.Add(panel);
|
|
476
|
+
|
|
477
|
+
var report = UiScenarioApplier.Apply(root, ScenarioWithSets(
|
|
478
|
+
new UiSetOperation("#label", "text", "after", "$test.set[0]"),
|
|
479
|
+
new UiSetOperation("#field", "value", "typed", "$test.set[1]"),
|
|
480
|
+
new UiSetOperation("#toggle", "value", true, "$test.set[2]"),
|
|
481
|
+
new UiSetOperation("#panel", "display", "none", "$test.set[3]"),
|
|
482
|
+
new UiSetOperation("#panel", "class:highlight", true, "$test.set[4]"),
|
|
483
|
+
new UiSetOperation("#label", "enabled", false, "$test.set[5]"),
|
|
484
|
+
new UiSetOperation("#field", "tooltip", "hint", "$test.set[6]"),
|
|
485
|
+
new UiSetOperation("#toggle", "visibility", "hidden", "$test.set[7]")));
|
|
486
|
+
|
|
487
|
+
Assert.That(report.SetCount, Is.EqualTo(8));
|
|
488
|
+
Assert.That(label.text, Is.EqualTo("after"));
|
|
489
|
+
Assert.That(field.value, Is.EqualTo("typed"));
|
|
490
|
+
Assert.That(toggle.value, Is.True);
|
|
491
|
+
Assert.That(panel.style.display.value, Is.EqualTo(DisplayStyle.None));
|
|
492
|
+
Assert.That(panel.ClassListContains("highlight"), Is.True);
|
|
493
|
+
Assert.That(label.enabledSelf, Is.False);
|
|
494
|
+
Assert.That(field.tooltip, Is.EqualTo("hint"));
|
|
495
|
+
Assert.That(toggle.style.visibility.value, Is.EqualTo(Visibility.Hidden));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
[Test]
|
|
499
|
+
public void Apply_SetValueTypeMismatch_ReportsCodeAndExactLocation()
|
|
500
|
+
{
|
|
501
|
+
var root = new VisualElement();
|
|
502
|
+
root.Add(new Toggle { name = "toggle" });
|
|
503
|
+
|
|
504
|
+
var exception = Assert.Throws<UiScenarioException>(() => UiScenarioApplier.Apply(
|
|
505
|
+
root,
|
|
506
|
+
ScenarioWithSets(new UiSetOperation("#toggle", "value", "yes", "$test.set[0]"))));
|
|
507
|
+
|
|
508
|
+
Assert.That(exception.Code, Is.EqualTo("ui.set-value-type"));
|
|
509
|
+
Assert.That(exception.Location, Is.EqualTo("$test.set[0].value"));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
[Test]
|
|
513
|
+
public void List_ReportsDocumentsAndFixturesIncludingInvalidOnes()
|
|
514
|
+
{
|
|
515
|
+
var validPath = WriteFixture(
|
|
516
|
+
"Valid.ucp-ui.json",
|
|
517
|
+
"{\"schemaVersion\":0,\"document\":\"./Document.uxml\",\"states\":{\"default\":{}}}");
|
|
518
|
+
var invalidPath = WriteFixture(
|
|
519
|
+
"Invalid.ucp-ui.json",
|
|
520
|
+
"{\"schemaVersion\":0,\"document\":\"./Document.uxml\",\"bogus\":1,\"states\":{\"default\":{}}}");
|
|
521
|
+
|
|
522
|
+
var result = (Dictionary<string, object>)UiScenarioLoader.List(
|
|
523
|
+
new Dictionary<string, object> { ["path"] = TestFolder });
|
|
524
|
+
var items = ((List<object>)result["items"]).Cast<Dictionary<string, object>>().ToList();
|
|
525
|
+
|
|
526
|
+
Assert.That(Convert.ToInt32(result["count"]), Is.EqualTo(4));
|
|
527
|
+
Assert.That(result["truncated"], Is.False);
|
|
528
|
+
Assert.That(
|
|
529
|
+
items.Select(item => item["target"]),
|
|
530
|
+
Is.EquivalentTo(new object[] { DocumentPath, RowPath, validPath, invalidPath }));
|
|
531
|
+
Assert.That(items.Count(item => Equals(item["type"], "uxml")), Is.EqualTo(2));
|
|
532
|
+
|
|
533
|
+
var valid = items.Single(item => Equals(item["target"], validPath));
|
|
534
|
+
Assert.That(valid["valid"], Is.True);
|
|
535
|
+
Assert.That(valid["defaultState"], Is.EqualTo("default"));
|
|
536
|
+
|
|
537
|
+
var invalid = items.Single(item => Equals(item["target"], invalidPath));
|
|
538
|
+
Assert.That(invalid["valid"], Is.False);
|
|
539
|
+
var diagnostic = (Dictionary<string, object>)((List<object>)invalid["diagnostics"])[0];
|
|
540
|
+
Assert.That(diagnostic["code"], Is.EqualTo("ui.unknown-field"));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private static UiResolvedScenario ScenarioWithSets(params UiSetOperation[] sets)
|
|
544
|
+
{
|
|
545
|
+
return new UiResolvedScenario(
|
|
546
|
+
DocumentPath,
|
|
547
|
+
null,
|
|
548
|
+
"default",
|
|
549
|
+
DocumentPath,
|
|
550
|
+
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(DocumentPath),
|
|
551
|
+
new Dictionary<string, object>(),
|
|
552
|
+
sets,
|
|
553
|
+
Array.Empty<UiCollectionDefinition>(),
|
|
554
|
+
new UiViewport(),
|
|
555
|
+
new UiSettleOptions());
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private static Dictionary<string, object> Item(string name)
|
|
559
|
+
{
|
|
560
|
+
return new Dictionary<string, object> { ["name"] = name };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private static string WriteFixture(string filename, string contents)
|
|
564
|
+
{
|
|
565
|
+
var assetPath = TestFolder + "/" + filename;
|
|
566
|
+
File.WriteAllText(AbsolutePath(assetPath), contents);
|
|
567
|
+
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate);
|
|
568
|
+
return assetPath;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private static void DeleteTestFolder()
|
|
572
|
+
{
|
|
573
|
+
if (AssetDatabase.IsValidFolder(TestFolder))
|
|
574
|
+
AssetDatabase.DeleteAsset(TestFolder);
|
|
575
|
+
else if (Directory.Exists(AbsolutePath(TestFolder)))
|
|
576
|
+
Directory.Delete(AbsolutePath(TestFolder), true);
|
|
577
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private static string AbsolutePath(string assetPath)
|
|
581
|
+
{
|
|
582
|
+
var projectRoot = Path.GetDirectoryName(Application.dataPath);
|
|
583
|
+
return Path.Combine(projectRoot, assetPath.Replace('/', Path.DirectorySeparatorChar));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
#endif
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "UCP.Bridge.Runtime",
|
|
3
|
-
"rootNamespace": "UCP.Bridge.Runtime",
|
|
4
|
-
"references": [],
|
|
5
|
-
"includePlatforms": [],
|
|
6
|
-
"excludePlatforms": [],
|
|
7
|
-
"allowUnsafeCode": false,
|
|
8
|
-
"overrideReferences": false,
|
|
9
|
-
"precompiledReferences": [],
|
|
10
|
-
"autoReferenced": true,
|
|
11
|
-
"defineConstraints": [],
|
|
12
|
-
"versionDefines": [],
|
|
13
|
-
"noEngineReferences": false
|
|
14
|
-
}
|