@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.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/bridge/com.ucp.bridge/CHANGELOG.md +48 -0
  3. package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +55 -6
  4. package/bridge/com.ucp.bridge/Editor/Bridge/EditorStateSummary.cs +165 -0
  5. package/bridge/com.ucp.bridge/Editor/Bridge/EditorStateSummary.cs.meta +11 -0
  6. package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +22 -5
  7. package/bridge/com.ucp.bridge/Editor/Controllers/EditorController.cs +1 -0
  8. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +10 -10
  9. package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +71 -0
  10. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
  11. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +4 -4
  12. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +3 -3
  13. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +6 -6
  14. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +7 -7
  15. package/bridge/com.ucp.bridge/Editor/Controllers/RecordingController.cs +8 -0
  16. package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +3 -3
  17. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +3 -3
  18. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +5 -5
  19. package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +1 -1
  20. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +1 -1
  21. package/bridge/com.ucp.bridge/Editor/Controllers/UiController.cs +70 -0
  22. package/bridge/com.ucp.bridge/Editor/Controllers/UiController.cs.meta +11 -0
  23. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +1 -1
  24. package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +1 -1
  25. package/bridge/com.ucp.bridge/Editor/Ui/UiHost.cs +468 -0
  26. package/bridge/com.ucp.bridge/Editor/Ui/UiHost.cs.meta +11 -0
  27. package/bridge/com.ucp.bridge/Editor/Ui/UiLintService.cs +554 -0
  28. package/bridge/com.ucp.bridge/Editor/Ui/UiLintService.cs.meta +11 -0
  29. package/bridge/com.ucp.bridge/Editor/Ui/UiOperationManager.cs +870 -0
  30. package/bridge/com.ucp.bridge/Editor/Ui/UiOperationManager.cs.meta +11 -0
  31. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioApplier.cs +496 -0
  32. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioApplier.cs.meta +11 -0
  33. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioLoader.cs +854 -0
  34. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioLoader.cs.meta +11 -0
  35. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioModels.cs +374 -0
  36. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioModels.cs.meta +11 -0
  37. package/bridge/com.ucp.bridge/Editor/Ui/UiVisualTreeInspector.cs +690 -0
  38. package/bridge/com.ucp.bridge/Editor/Ui/UiVisualTreeInspector.cs.meta +11 -0
  39. package/bridge/com.ucp.bridge/{Runtime.meta → Editor/Ui.meta} +1 -1
  40. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +79 -4
  41. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +2 -2
  42. package/bridge/com.ucp.bridge/Tests/Editor/UiControllerTests.cs +547 -0
  43. package/bridge/com.ucp.bridge/Tests/Editor/UiControllerTests.cs.meta +11 -0
  44. package/bridge/com.ucp.bridge/Tests/Editor/UiLintServiceTests.cs +253 -0
  45. package/bridge/com.ucp.bridge/Tests/Editor/UiLintServiceTests.cs.meta +11 -0
  46. package/bridge/com.ucp.bridge/Tests/Editor/UiScenarioTests.cs +587 -0
  47. package/bridge/com.ucp.bridge/Tests/Editor/UiScenarioTests.cs.meta +11 -0
  48. package/bridge/com.ucp.bridge/package.json +1 -1
  49. package/package.json +1 -1
  50. package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef +0 -14
  51. package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef.meta +0 -7
@@ -0,0 +1,547 @@
1
+ #if UNITY_6000_0_OR_NEWER
2
+ using System;
3
+ using System.Collections.Generic;
4
+ using NUnit.Framework;
5
+ using UnityEngine.UIElements;
6
+
7
+ namespace UCP.Bridge.Tests
8
+ {
9
+ public sealed class UiControllerTests
10
+ {
11
+ [Test]
12
+ public void Register_ExposesUiCommandFamily()
13
+ {
14
+ var router = new CommandRouter();
15
+ UiController.Register(router);
16
+
17
+ Assert.That(router.HasMethod("ui/list"), Is.True);
18
+ Assert.That(router.HasMethod("ui/lint"), Is.True);
19
+ Assert.That(router.HasMethod("ui/inspect"), Is.True);
20
+ Assert.That(router.HasMethod("ui/screenshot"), Is.True);
21
+ Assert.That(router.HasMethod("ui/check"), Is.True);
22
+ Assert.That(router.HasMethod("ui/status"), Is.True);
23
+ }
24
+
25
+ [Test]
26
+ public void Inspect_ReturnsAsyncStartEnvelopeBeforeResolvingTarget()
27
+ {
28
+ try
29
+ {
30
+ var router = new CommandRouter();
31
+ UiController.Register(router);
32
+
33
+ var response = router.Dispatch(
34
+ "ui/inspect",
35
+ 1,
36
+ "{\"target\":\"Assets/DoesNotExist.uxml\"}");
37
+
38
+ Assert.That(response.error, Is.Null);
39
+ var result = (Dictionary<string, object>)response.result;
40
+ Assert.That(result["status"], Is.EqualTo("started"));
41
+ Assert.That(result["operation"], Is.EqualTo("inspect"));
42
+ Assert.That(result["operationId"].ToString(), Does.StartWith("ui-"));
43
+ }
44
+ finally
45
+ {
46
+ // Prevent the intentionally unresolved target from running on a later
47
+ // editor update and polluting the test run's console guard.
48
+ UiOperationManager.ResetForTests();
49
+ }
50
+ }
51
+
52
+ [Test]
53
+ public void OperationManager_ConstructorFailureCompletesAndAdvancesQueue()
54
+ {
55
+ UiOperationManager.ResetForTests();
56
+ try
57
+ {
58
+ var failedStart = UiOperationManager.Start("inspect", new Dictionary<string, object>
59
+ {
60
+ ["target"] = "Assets/DoesNotExist.uxml",
61
+ ["failOnWarnings"] = "not-a-boolean"
62
+ });
63
+ var followingStart = UiOperationManager.Start("inspect", new Dictionary<string, object>
64
+ {
65
+ ["failOnWarnings"] = "also-not-a-boolean"
66
+ });
67
+
68
+ UiOperationManager.TickForTests();
69
+
70
+ var failed = UiOperationManager.Status(failedStart["operationId"].ToString());
71
+ Assert.That(failed["status"], Is.EqualTo("failed"));
72
+ var error = (Dictionary<string, object>)failed["error"];
73
+ Assert.That(error["code"], Is.EqualTo("invalid_params"));
74
+
75
+ var following = UiOperationManager.Status(followingStart["operationId"].ToString());
76
+ Assert.That(following["status"], Is.EqualTo("failed"));
77
+ Assert.That(
78
+ ((Dictionary<string, object>)following["error"])["code"],
79
+ Is.EqualTo("invalid_params"));
80
+ }
81
+ finally
82
+ {
83
+ UiOperationManager.ResetForTests();
84
+ }
85
+ }
86
+
87
+ [Test]
88
+ public void OperationManager_ShutdownFailsActiveAndQueuedOperations()
89
+ {
90
+ UiOperationManager.ResetForTests();
91
+ try
92
+ {
93
+ var active = UiOperationManager.Start("inspect", new Dictionary<string, object>());
94
+ var queued = UiOperationManager.Start("inspect", new Dictionary<string, object>());
95
+ // Activate without advancing initialization (which would resolve a target).
96
+ UiOperationManager.ActivateNextForTests();
97
+ Assert.That(UiOperationManager.Status(active["operationId"].ToString())["status"], Is.EqualTo("running"));
98
+ UiOperationManager.Shutdown();
99
+ foreach (var start in new[] { active, queued })
100
+ {
101
+ var status = UiOperationManager.Status(start["operationId"].ToString());
102
+ Assert.That(status["status"], Is.EqualTo("failed"));
103
+ Assert.That(((Dictionary<string, object>)status["error"])["code"], Is.EqualTo("editor_shutdown"));
104
+ }
105
+ UiOperationManager.TickForTests();
106
+ Assert.That(UiOperationManager.Status(queued["operationId"].ToString())["status"], Is.EqualTo("failed"));
107
+ }
108
+ finally
109
+ {
110
+ UiOperationManager.ResetForTests();
111
+ }
112
+ }
113
+
114
+ [Test]
115
+ public void Audit_RetainsFirstErrorsWhenErrorsExhaustBudget()
116
+ {
117
+ var report = new UiApplyReport();
118
+ report.AddDiagnostic("warning", "test", "displaced warning");
119
+ for (var index = 0; index < 201; index++)
120
+ report.AddDiagnostic("error", "test", "error " + index);
121
+ var audit = UiAudit.Run(new VisualElement(), report, Array.Empty<UiCollectionMetadata>());
122
+ Assert.That(audit["errorCount"], Is.EqualTo(201));
123
+ Assert.That(audit["warningCount"], Is.EqualTo(1));
124
+ Assert.That(audit["diagnosticsTruncated"], Is.True);
125
+ Assert.That((List<object>)audit["warnings"], Is.Empty);
126
+ var errors = (List<object>)audit["errors"];
127
+ Assert.That(errors, Has.Count.EqualTo(200));
128
+ Assert.That(((Dictionary<string, object>)errors[199])["message"], Is.EqualTo("error 199"));
129
+ }
130
+
131
+ [Test]
132
+ public void Cleanup_RunsEveryActionAndRetainsFirstFailure()
133
+ {
134
+ var visited = new List<int>();
135
+ var expected = new InvalidOperationException("first");
136
+
137
+ var result = UiCleanup.RunAll(
138
+ () =>
139
+ {
140
+ visited.Add(1);
141
+ throw expected;
142
+ },
143
+ () => visited.Add(2),
144
+ () =>
145
+ {
146
+ visited.Add(3);
147
+ throw new InvalidOperationException("later");
148
+ });
149
+
150
+ Assert.That(visited, Is.EqualTo(new[] { 1, 2, 3 }));
151
+ Assert.That(result, Is.SameAs(expected));
152
+ }
153
+
154
+ [Test]
155
+ public void OperationRecord_PreservesResultWhenCleanupFails()
156
+ {
157
+ var record = new UiOperationRecord(
158
+ "ui-test",
159
+ "inspect",
160
+ new Dictionary<string, object>());
161
+ record.MarkRunning();
162
+ record.Complete(
163
+ new Dictionary<string, object> { ["stateCount"] = 1 },
164
+ UiRenderOperation.BuildError(new UiOperationException(
165
+ "cleanup_failed",
166
+ "cleanup failed")));
167
+
168
+ var envelope = record.Envelope();
169
+ Assert.That(envelope["status"], Is.EqualTo("failed"));
170
+ Assert.That(envelope.ContainsKey("result"), Is.True);
171
+ Assert.That(envelope.ContainsKey("error"), Is.True);
172
+ Assert.That(
173
+ ((Dictionary<string, object>)envelope["error"])["code"],
174
+ Is.EqualTo("cleanup_failed"));
175
+ }
176
+
177
+ [Test]
178
+ public void CaptureArtifactFileName_DisambiguatesLossyStateTokens()
179
+ {
180
+ var first = UiRenderOperation.CaptureArtifactFileName(
181
+ "Assets/Panel.uxml",
182
+ "a/b",
183
+ 0,
184
+ "ui-test");
185
+ var second = UiRenderOperation.CaptureArtifactFileName(
186
+ "Assets/Panel.uxml",
187
+ "a-b",
188
+ 1,
189
+ "ui-test");
190
+
191
+ Assert.That(first, Is.EqualTo("Panel-a-b-s0000-ui-test.png"));
192
+ Assert.That(second, Is.EqualTo("Panel-a-b-s0001-ui-test.png"));
193
+ Assert.That(first, Is.Not.EqualTo(second));
194
+ Assert.That(
195
+ UiRenderOperation.CaptureArtifactFileName("Assets/Panel.uxml", "a/b", 0, "ui-test"),
196
+ Is.EqualTo(first));
197
+ }
198
+
199
+ [Test]
200
+ public void CaptureArtifactFileName_DropsTheScenarioSuffix()
201
+ {
202
+ Assert.That(
203
+ UiRenderOperation.CaptureArtifactFileName(
204
+ "Assets/UI/Inventory.ucp-ui.json",
205
+ "populated",
206
+ 1,
207
+ "ui-test"),
208
+ Is.EqualTo("Inventory-populated-s0001-ui-test.png"));
209
+ }
210
+
211
+ [Test]
212
+ public void Inspector_AppliesSimpleQueryAndElementBound()
213
+ {
214
+ var root = new VisualElement { name = "root" };
215
+ var first = new Label("First") { name = "first" };
216
+ first.AddToClassList("match");
217
+ var second = new Label("Second") { name = "second" };
218
+ second.AddToClassList("match");
219
+ root.Add(first);
220
+ root.Add(second);
221
+
222
+ var options = UiInspectOptions.Parse(new Dictionary<string, object>
223
+ {
224
+ ["query"] = ".match",
225
+ ["detail"] = "summary",
226
+ ["max"] = 1L
227
+ });
228
+ var result = UiVisualTreeInspector.Inspect(
229
+ root,
230
+ options,
231
+ Array.Empty<UiCollectionMetadata>());
232
+
233
+ Assert.That(Convert.ToInt32(result["matchCount"]), Is.EqualTo(2));
234
+ Assert.That(Convert.ToInt32(result["returnedElementCount"]), Is.EqualTo(1));
235
+ Assert.That(Convert.ToBoolean(result["truncated"]), Is.True);
236
+ }
237
+
238
+ [Test]
239
+ public void InspectorAndGeometrySampler_IncludePhysicalChildrenOfListView()
240
+ {
241
+ var list = new ListView();
242
+ var row = new Label("Before") { name = "row-label" };
243
+ // A ListView owns its physical hierarchy and exposes no contentContainer.
244
+ // Model a realized row without needing a graphics device for this regression.
245
+ list.hierarchy.Add(row);
246
+ Assert.That(list.contentContainer, Is.Null);
247
+
248
+ var snapshot = UiVisualTreeInspector.Inspect(list,
249
+ UiInspectOptions.Parse(new Dictionary<string, object> { ["query"] = "#row-label" }),
250
+ Array.Empty<UiCollectionMetadata>());
251
+ Assert.That(snapshot["matchCount"], Is.EqualTo(1));
252
+ var fullSnapshot = UiVisualTreeInspector.Inspect(list,
253
+ UiInspectOptions.Parse(new Dictionary<string, object> { ["depth"] = 64 }),
254
+ Array.Empty<UiCollectionMetadata>());
255
+ Assert.That(MiniJson.Serialize(fullSnapshot), Does.Contain("row-label"));
256
+
257
+ var before = UiGeometrySampler.Measure(list);
258
+ row.text = "After";
259
+ Assert.That(UiGeometrySampler.Measure(list).Hash, Is.Not.EqualTo(before.Hash));
260
+ }
261
+
262
+ [Test]
263
+ public void Audit_ReportsDuplicateStaticNames()
264
+ {
265
+ var root = new VisualElement();
266
+ root.Add(new Label { name = "duplicate" });
267
+ root.Add(new Label { name = "duplicate" });
268
+
269
+ var result = UiAudit.Run(
270
+ root,
271
+ new UiApplyReport(),
272
+ Array.Empty<UiCollectionMetadata>());
273
+
274
+ Assert.That(Convert.ToInt32(result["errorCount"]), Is.EqualTo(0));
275
+ Assert.That(Convert.ToInt32(result["warningCount"]), Is.EqualTo(1));
276
+ var warnings = (List<object>)result["warnings"];
277
+ var warning = (Dictionary<string, object>)warnings[0];
278
+ Assert.That(warning["code"], Is.EqualTo("duplicate_name"));
279
+ }
280
+
281
+ [Test]
282
+ public void Audit_UsesOneGlobalDiagnosticBudgetIncludingDuplicateNames()
283
+ {
284
+ var report = new UiApplyReport();
285
+ for (var index = 0; index < 199; index++)
286
+ report.AddDiagnostic("error", "test", "test diagnostic");
287
+
288
+ var root = new VisualElement();
289
+ root.Add(new VisualElement { name = "first-duplicate" });
290
+ root.Add(new VisualElement { name = "first-duplicate" });
291
+ root.Add(new VisualElement { name = "second-duplicate" });
292
+ root.Add(new VisualElement { name = "second-duplicate" });
293
+
294
+ var result = UiAudit.Run(
295
+ root,
296
+ report,
297
+ Array.Empty<UiCollectionMetadata>());
298
+
299
+ Assert.That(Convert.ToInt32(result["errorCount"]), Is.EqualTo(199));
300
+ Assert.That(Convert.ToInt32(result["warningCount"]), Is.EqualTo(2));
301
+ Assert.That((List<object>)result["errors"], Has.Count.EqualTo(199));
302
+ Assert.That((List<object>)result["warnings"], Has.Count.EqualTo(1));
303
+ Assert.That(result["diagnosticsTruncated"], Is.True);
304
+ }
305
+
306
+ [TestCase(199)]
307
+ [TestCase(200)]
308
+ [TestCase(201)]
309
+ public void Audit_CountsErrorsBeyondReturnedDiagnosticLimit(int warningCount)
310
+ {
311
+ var report = new UiApplyReport();
312
+ for (var index = 0; index < warningCount; index++)
313
+ report.AddDiagnostic("warning", "test", "warning");
314
+ report.AddDiagnostic("error", "test", "error after warnings");
315
+
316
+ var result = UiAudit.Run(new VisualElement(), report, Array.Empty<UiCollectionMetadata>());
317
+ var errors = (List<object>)result["errors"];
318
+ var warnings = (List<object>)result["warnings"];
319
+
320
+ Assert.That(result["passed"], Is.False);
321
+ Assert.That(result["errorCount"], Is.EqualTo(1));
322
+ Assert.That(result["warningCount"], Is.EqualTo(warningCount));
323
+ Assert.That(errors.Count + warnings.Count, Is.EqualTo(200));
324
+ Assert.That(errors, Has.Count.EqualTo(1));
325
+ Assert.That(((Dictionary<string, object>)errors[0])["message"], Is.EqualTo("error after warnings"));
326
+ Assert.That(warnings, Has.Count.EqualTo(199));
327
+ Assert.That(result["diagnosticsTruncated"], Is.EqualTo(warningCount >= 200));
328
+ }
329
+
330
+ [Test]
331
+ public void Audit_ContinuesCheckingTreeAfterApplicationDiagnosticsFillLimit()
332
+ {
333
+ var report = new UiApplyReport();
334
+ for (var index = 0; index < 200; index++)
335
+ report.AddDiagnostic("warning", "test", "warning");
336
+ var root = new VisualElement();
337
+ root.Add(new Label { name = "duplicate" });
338
+ root.Add(new Label { name = "duplicate" });
339
+
340
+ var result = UiAudit.Run(root, report, Array.Empty<UiCollectionMetadata>());
341
+
342
+ Assert.That(result["passed"], Is.True);
343
+ Assert.That(result["errorCount"], Is.EqualTo(0));
344
+ Assert.That(result["warningCount"], Is.EqualTo(201));
345
+ Assert.That((List<object>)result["warnings"], Has.Count.EqualTo(200));
346
+ Assert.That(result["diagnosticsTruncated"], Is.True);
347
+ }
348
+
349
+ [Test]
350
+ public void Audit_IgnoresHiddenFocusableElementsAndUnityInternalNames()
351
+ {
352
+ var root = new VisualElement();
353
+ var displayNone = new VisualElement();
354
+ displayNone.style.display = DisplayStyle.None;
355
+ displayNone.Add(new VisualElement { name = "hidden-by-display", focusable = true });
356
+ root.Add(displayNone);
357
+
358
+ var visibilityHidden = new VisualElement();
359
+ visibilityHidden.style.visibility = Visibility.Hidden;
360
+ visibilityHidden.Add(new VisualElement { name = "hidden-by-visibility", focusable = true });
361
+ root.Add(visibilityHidden);
362
+
363
+ root.Add(new VisualElement { name = "unity-generated" });
364
+ root.Add(new VisualElement { name = "unity-generated" });
365
+ root.Add(new VisualElement { name = "authored-name" });
366
+ root.Add(new VisualElement { name = "authored-name" });
367
+
368
+ var result = UiAudit.Run(
369
+ root,
370
+ new UiApplyReport(),
371
+ Array.Empty<UiCollectionMetadata>());
372
+ var warnings = (List<object>)result["warnings"];
373
+
374
+ Assert.That(warnings, Has.Count.EqualTo(1));
375
+ var warning = (Dictionary<string, object>)warnings[0];
376
+ Assert.That(warning["code"], Is.EqualTo("duplicate_name"));
377
+ Assert.That(
378
+ ((Dictionary<string, object>)warning["element"])["name"],
379
+ Is.EqualTo("authored-name"));
380
+ }
381
+
382
+ [Test]
383
+ public void OperationDurationLimit_IsOverallAndReportsCompletedStates()
384
+ {
385
+ Assert.That(
386
+ UiOperationManager.HasExceededOperationDuration(
387
+ 10.0,
388
+ 10.0 + UiOperationManager.MaxOperationDurationSeconds),
389
+ Is.False);
390
+ Assert.That(
391
+ UiOperationManager.HasExceededOperationDuration(
392
+ 10.0,
393
+ 10.001 + UiOperationManager.MaxOperationDurationSeconds),
394
+ Is.True);
395
+
396
+ var error = UiRenderOperation.BuildError(
397
+ UiOperationManager.CreateOperationTimeout("check", 3, 301.0));
398
+ Assert.That(error["code"], Is.EqualTo("timeout"));
399
+ var details = (Dictionary<string, object>)error["details"];
400
+ Assert.That(details["scope"], Is.EqualTo("operation"));
401
+ Assert.That(Convert.ToInt32(details["completedStateCount"]), Is.EqualTo(3));
402
+ }
403
+
404
+ [Test]
405
+ public void GeometrySampler_IsDeterministicForUnchangedTree()
406
+ {
407
+ var root = new VisualElement { name = "root" };
408
+ root.Add(new Label("Stable") { name = "label" });
409
+
410
+ var first = UiGeometrySampler.Measure(root);
411
+ var second = UiGeometrySampler.Measure(root);
412
+
413
+ Assert.That(first.Hash, Is.EqualTo(second.Hash));
414
+ Assert.That(first.ElementCount, Is.EqualTo(2));
415
+ Assert.That(first.PanelAttached, Is.False);
416
+ Assert.That(first.IsValid, Is.False);
417
+ }
418
+
419
+ [Test]
420
+ public void JsonSanitizer_ReplacesNonFiniteNumbersWithNull()
421
+ {
422
+ var result = (Dictionary<string, object>)UiJsonSanitizer.Sanitize(
423
+ new Dictionary<string, object>
424
+ {
425
+ ["nan"] = double.NaN,
426
+ ["infinity"] = float.PositiveInfinity,
427
+ ["finite"] = 4.25
428
+ });
429
+
430
+ Assert.That(result["nan"], Is.Null);
431
+ Assert.That(result["infinity"], Is.Null);
432
+ Assert.That(result["finite"], Is.EqualTo(4.25));
433
+ }
434
+
435
+ [Test]
436
+ public void JsonSanitizer_NormalizesScalarsUnsupportedByMiniJson()
437
+ {
438
+ var result = (Dictionary<string, object>)UiJsonSanitizer.Sanitize(
439
+ new Dictionary<string, object>
440
+ {
441
+ ["character"] = 'x',
442
+ ["byte"] = (byte)12,
443
+ ["signedByte"] = (sbyte)-4,
444
+ ["short"] = (short)-123,
445
+ ["unsignedShort"] = ushort.MaxValue,
446
+ ["unsignedInt"] = uint.MaxValue,
447
+ ["unsignedLong"] = ulong.MaxValue,
448
+ ["decimal"] = 123.4500m
449
+ });
450
+
451
+ Assert.That(result["character"], Is.EqualTo("x"));
452
+ Assert.That(result["byte"], Is.TypeOf<int>().And.EqualTo(12));
453
+ Assert.That(result["signedByte"], Is.TypeOf<int>().And.EqualTo(-4));
454
+ Assert.That(result["short"], Is.TypeOf<int>().And.EqualTo(-123));
455
+ Assert.That(result["unsignedShort"], Is.TypeOf<int>().And.EqualTo(ushort.MaxValue));
456
+ Assert.That(result["unsignedInt"], Is.TypeOf<long>().And.EqualTo((long)uint.MaxValue));
457
+ Assert.That(result["unsignedLong"], Is.EqualTo(ulong.MaxValue.ToString()));
458
+ Assert.That(result["decimal"], Is.EqualTo("123.4500"));
459
+
460
+ var serialized = MiniJson.Serialize(result);
461
+ Assert.That(serialized, Does.Not.Contain("{}"));
462
+ Assert.That(MiniJson.Deserialize(serialized), Is.TypeOf<Dictionary<string, object>>());
463
+ }
464
+
465
+ [TestCase("#row-label")]
466
+ [TestCase(".card_item-2")]
467
+ [TestCase("Label")]
468
+ [TestCase("UnityEngine.UIElements.Label")]
469
+ public void SimpleQuery_AcceptsOneNameClassOrTypeSelector(string query)
470
+ {
471
+ Assert.DoesNotThrow(() => UiSimpleQuery.Validate(query));
472
+ }
473
+
474
+ [TestCase("#toolbar Button")]
475
+ [TestCase("#a > .b")]
476
+ [TestCase(".a.b")]
477
+ [TestCase("#a#b")]
478
+ [TestCase("Label:hover")]
479
+ [TestCase("#")]
480
+ public void SimpleQuery_RejectsCompoundSelectorsInsteadOfMatchingNothing(string query)
481
+ {
482
+ Assert.Throws<ArgumentException>(() => UiSimpleQuery.Validate(query));
483
+ }
484
+
485
+ [Test]
486
+ public void SimpleQuery_MatchesByNameClassShortTypeAndFullType()
487
+ {
488
+ var label = new Label { name = "title" };
489
+ label.AddToClassList("heading");
490
+
491
+ Assert.That(UiSimpleQuery.Matches(label, "#title"), Is.True);
492
+ Assert.That(UiSimpleQuery.Matches(label, ".heading"), Is.True);
493
+ Assert.That(UiSimpleQuery.Matches(label, "label"), Is.True);
494
+ Assert.That(UiSimpleQuery.Matches(label, "UnityEngine.UIElements.Label"), Is.True);
495
+ Assert.That(UiSimpleQuery.Matches(label, "#heading"), Is.False);
496
+ Assert.That(UiSimpleQuery.Matches(label, "Button"), Is.False);
497
+ }
498
+
499
+ [Test]
500
+ public void Status_ReportsUnknownOperationsAndRejectsMissingIds()
501
+ {
502
+ var router = new CommandRouter();
503
+ UiController.Register(router);
504
+
505
+ var unknown = router.Dispatch("ui/status", 1, "{\"operationId\":\"ui-does-not-exist\"}");
506
+ Assert.That(unknown.error, Is.Null);
507
+ var result = (Dictionary<string, object>)unknown.result;
508
+ Assert.That(result["found"], Is.False);
509
+ Assert.That(result["operationId"], Is.EqualTo("ui-does-not-exist"));
510
+
511
+ var missing = router.Dispatch("ui/status", 2, "{}");
512
+ Assert.That(missing.error, Is.Not.Null);
513
+ Assert.That(missing.result, Is.Null);
514
+ }
515
+
516
+ [Test]
517
+ public void ResultEnvelope_IsSanitizedBeforeItReachesMiniJson()
518
+ {
519
+ var record = new UiOperationRecord("ui-json", "check", new Dictionary<string, object>());
520
+ record.MarkRunning();
521
+ record.Complete(
522
+ new Dictionary<string, object>
523
+ {
524
+ ["passed"] = false,
525
+ ["errorCount"] = 1,
526
+ ["states"] = new List<object>
527
+ {
528
+ new Dictionary<string, object>
529
+ {
530
+ ["ratio"] = float.NaN,
531
+ ["mode"] = UiCollectionMode.ListView
532
+ }
533
+ }
534
+ },
535
+ null);
536
+
537
+ var parsed = (Dictionary<string, object>)MiniJson.Deserialize(MiniJson.Serialize(record.Envelope()));
538
+ Assert.That(parsed["status"], Is.EqualTo("completed"));
539
+ var result = (Dictionary<string, object>)parsed["result"];
540
+ Assert.That(result["passed"], Is.False);
541
+ var state = (Dictionary<string, object>)((List<object>)result["states"])[0];
542
+ Assert.That(state["ratio"], Is.Null);
543
+ Assert.That(state["mode"], Is.EqualTo("ListView"));
544
+ }
545
+ }
546
+ }
547
+ #endif
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: f28fa266893c4194b8fc5bf7db17333f
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: