gdharness 1.3.42 → 1.3.44

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 (106) hide show
  1. package/build/cli.js +383 -243
  2. package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
  3. package/build/godot/addons/gdharness_editor/animation_reads.gd +55 -33
  4. package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
  5. package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +83 -42
  7. package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
  8. package/build/godot/addons/gdharness_editor/headless_check.gd +68 -44
  9. package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
  10. package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
  11. package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
  12. package/build/godot/addons/gdharness_editor/reading.gd +51 -17
  13. package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +127 -80
  15. package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
  16. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
  17. package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
  18. package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
  19. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +37 -27
  20. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +79 -49
  21. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
  22. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +65 -42
  23. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
  24. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
  25. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
  26. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
  27. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +166 -107
  28. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
  29. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +68 -41
  30. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
  31. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
  32. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +12 -5
  33. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +113 -67
  34. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
  35. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
  36. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
  37. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
  38. package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
  39. package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
  40. package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
  41. package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
  42. package/build/godot/addons/gdharness_runtime/reading.gd +51 -17
  43. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
  44. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +149 -147
  45. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
  46. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
  47. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
  49. package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
  50. package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
  51. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
  52. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +34 -17
  53. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
  54. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
  55. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
  56. package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
  57. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
  58. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
  59. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +15 -15
  60. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
  61. package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
  62. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
  63. package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
  64. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
  65. package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
  66. package/build/godot/addons/gdharness_tests/bench.gd +4 -5
  67. package/build/godot/addons/gdharness_tests/event.gd +10 -5
  68. package/build/godot/addons/gdharness_tests/share.gd +38 -18
  69. package/build/godot/addons/gdharness_tests/suites.gd +3 -3
  70. package/build/godot/addons/gdharness_tests/wait.gd +3 -2
  71. package/build/godot/loader/loader.gd +9 -9
  72. package/build/godot/operations/animation_reads.gd +55 -33
  73. package/build/godot/operations/audio_buses.gd +48 -19
  74. package/build/godot/operations/class_cache.gd +16 -11
  75. package/build/godot/operations/classdb_queries.gd +89 -44
  76. package/build/godot/operations/declared_values.gd +68 -32
  77. package/build/godot/operations/dependencies.gd +190 -109
  78. package/build/godot/operations/editor_tools.gd +10 -10
  79. package/build/godot/operations/export_presets.gd +100 -54
  80. package/build/godot/operations/file_reads.gd +83 -42
  81. package/build/godot/operations/file_walk.gd +12 -5
  82. package/build/godot/operations/gdscript_analysis.gd +86 -65
  83. package/build/godot/operations/gdscript_authoring.gd +83 -36
  84. package/build/godot/operations/godot_operations.gd +17 -11
  85. package/build/godot/operations/import_pipeline.gd +110 -59
  86. package/build/godot/operations/input_actions.gd +62 -39
  87. package/build/godot/operations/patterns.gd +3 -1
  88. package/build/godot/operations/plugins.gd +36 -16
  89. package/build/godot/operations/project_config.gd +61 -32
  90. package/build/godot/operations/project_diagnostics.gd +24 -11
  91. package/build/godot/operations/project_file.gd +16 -16
  92. package/build/godot/operations/project_validation.gd +71 -44
  93. package/build/godot/operations/property_paths.gd +22 -12
  94. package/build/godot/operations/property_values.gd +143 -78
  95. package/build/godot/operations/reading.gd +51 -17
  96. package/build/godot/operations/rename_checks.gd +2 -2
  97. package/build/godot/operations/resource_files.gd +11 -9
  98. package/build/godot/operations/same_values.gd +4 -4
  99. package/build/godot/operations/scene_connections.gd +27 -13
  100. package/build/godot/operations/script_check.gd +52 -49
  101. package/build/godot/operations/serialisation.gd +75 -51
  102. package/build/godot/operations/setting_reads.gd +7 -6
  103. package/build/godot/operations/value_shapes.gd +5 -6
  104. package/build/godot/tests/listed.gd +321 -268
  105. package/build/keeper.js +43 -43
  106. package/package.json +1 -1
@@ -1,19 +1,17 @@
1
1
  extends RefCounted
2
-
3
2
  ## What the running game tells a screen reader: its accessibility tree, as UI Automation reads it.
4
3
  ##
5
4
  ## The engine builds the tree only when something asks for it, and nothing on gdharness's desktop
6
5
  ## does, so a game's NOTIFICATION_ACCESSIBILITY_UPDATE never came and what its controls expose could
7
6
  ## not be checked. The engine offers no way to read the tree back, so the question is put the way a
8
- ## screen reader puts it: a UI Automation client started by the game lands on the game's own desktop,
9
- ## its first question makes the engine build the tree, and what it reads is what a screen reader is
10
- ## told, names left empty included.
7
+ ## screen reader puts it: a UI Automation client started by the game lands on the game's own
8
+ ## desktop, its first question makes the engine build the tree, and what it reads is what a screen
9
+ ## reader is told, names left empty included.
11
10
 
12
11
  const GdhRead = preload("reading.gd")
13
12
  const GdhSays = preload("runtime_says.gd")
14
-
15
- ## How long the client keeps walking the tree for it to settle: the engine answers the first question
16
- ## with its windows alone and fills them in over the next frames.
13
+ ## How long the client keeps walking the tree for it to settle: the engine answers the first
14
+ ## question with its windows alone and fills them in over the next frames.
17
15
  const SETTLE_MS: int = 8000
18
16
  ## How long a window with nothing in it is walked before the client stops: a game building its tree
19
17
  ## fills the window within a few frames of being asked, and one building none never does.
@@ -33,12 +31,11 @@ const MOST_LEVELS: int = 40
33
31
  ## Where an element holds its node's instance id between matching and placing, removed before the
34
32
  ## answer goes.
35
33
  const NODE_ID: String = "_node_instance"
36
-
37
34
  ## The client, as PowerShell, with $Handles, $Out, $Progress, $SettleMs, $EmptyMs and $MostLevels
38
- ## set before it. Each element answers what UI Automation names its role, its name, its value where it
39
- ## has one, its bounds in the window's pixels, and the actions a screen reader is offered on it. Each
40
- ## stage it reaches is marked in $Progress as it is reached, with the time, so a client stopped at the
41
- ## deadline still says how far it got.
35
+ ## set before it. Each element answers what UI Automation names its role, its name, its value where
36
+ ## it has one, its bounds in the window's pixels, and the actions a screen reader is offered on it.
37
+ ## Each stage it reaches is marked in $Progress as it is reached, with the time, so a client stopped
38
+ ## at the deadline still says how far it got.
42
39
  const WALK: String = r"""
43
40
  $ErrorActionPreference = 'Stop'
44
41
  function Answer($value) {
@@ -200,17 +197,17 @@ func get_accessibility(params: Dictionary) -> Dictionary:
200
197
  if _setting() != "always":
201
198
  also = (
202
199
  (
203
- ' and engineArgs ["--accessibility", "always"], since this project\'s'
204
- + " accessibility/general/accessibility_support is %s and the engine builds the tree"
205
- + " only when asked"
200
+ ' and engineArgs ["--accessibility", "always"], since this project\'s '
201
+ + "accessibility/general/accessibility_support is %s and the engine builds the "
202
+ + "tree only when asked"
206
203
  )
207
204
  % _setting()
208
205
  )
209
206
  return _refused(
210
207
  (
211
208
  (
212
- "A headless run has no window, so a screen reader is told nothing: start the run with"
213
- + " headless: false%s."
209
+ "A headless run has no window, so a screen reader is told nothing: start the "
210
+ + "run with headless: false%s."
214
211
  )
215
212
  % also
216
213
  )
@@ -219,8 +216,8 @@ func get_accessibility(params: Dictionary) -> Dictionary:
219
216
  return _refused(
220
217
  (
221
218
  (
222
- "The tree is read through UI Automation, the interface a Windows screen reader uses, and this"
223
- + " game runs on %s, whose screen readers ask it through another"
219
+ "The tree is read through UI Automation, the interface a Windows screen reader "
220
+ + "uses, and this game runs on %s, whose screen readers ask it through another"
224
221
  )
225
222
  % OS.get_name()
226
223
  )
@@ -239,7 +236,10 @@ func get_accessibility(params: Dictionary) -> Dictionary:
239
236
  var progress: String = out + ".progress"
240
237
  var script: String = (
241
238
  (
242
- "$Handles = @(%s)\n$Out = '%s'\n$Progress = '%s'\n$SettleMs = %d\n$EmptyMs = %d\n$MostLevels = %d\n"
239
+ (
240
+ "$Handles = @(%s)\n$Out = '%s'\n$Progress = '%s'\n$SettleMs = %d\n$EmptyMs = "
241
+ + "%d\n$MostLevels = %d\n"
242
+ )
243
243
  % [
244
244
  ", ".join(handles),
245
245
  out.replace("'", "''"),
@@ -271,9 +271,9 @@ func get_accessibility(params: Dictionary) -> Dictionary:
271
271
  if pid <= 0:
272
272
  return _refused("PowerShell could not be started from %s to read the tree" % shell)
273
273
  # Waited out a frame at a time rather than blocking: the client's questions are answered on the
274
- # game's main thread, which a blocking wait would hold, and neither side would move.
275
- # Two budgets, so a slow start is never charged to the walk: one for PowerShell to begin and load
276
- # UI Automation, and once it has, one for the walks.
274
+ # game's main thread, which a blocking wait would hold, and neither side would move. Two
275
+ # budgets, so a slow start is never charged to the walk: one for PowerShell to begin and load UI
276
+ # Automation, and once it has, one for the walks.
277
277
  var began_at: int = Time.get_ticks_msec()
278
278
  var deadline: int = _deadline(began_at, -1, handles.size())
279
279
  var walking: bool = false
@@ -299,7 +299,9 @@ func get_accessibility(params: Dictionary) -> Dictionary:
299
299
 
300
300
  var text: String = FileAccess.get_file_as_string(out)
301
301
  var took: Dictionary = _took(
302
- FileAccess.get_file_as_string(progress), started_ms, int(Time.get_unix_time_from_system() * 1000.0)
302
+ FileAccess.get_file_as_string(progress),
303
+ started_ms,
304
+ int(Time.get_unix_time_from_system() * 1000.0)
303
305
  )
304
306
  # Not checked: a file that is not there was never written, which the answer below says.
305
307
  var _removed: Error = DirAccess.remove_absolute(out)
@@ -309,7 +311,9 @@ func get_accessibility(params: Dictionary) -> Dictionary:
309
311
  return _refused("UI Automation answered nothing readable: %s" % text.left(500))
310
312
  var answer: Dictionary = read
311
313
  if answer.has("error"):
312
- return _refused("UI Automation could not read the game's windows: %s" % str(answer["error"]))
314
+ return _refused(
315
+ "UI Automation could not read the game's windows: %s" % str(answer["error"])
316
+ )
313
317
 
314
318
  var windows: Array = answer.get("windows", [])
315
319
  var made_by: Dictionary = {}
@@ -333,25 +337,28 @@ func get_accessibility(params: Dictionary) -> Dictionary:
333
337
  _unmark(window)
334
338
  elements += _count(window)
335
339
  unnamed += _unnamed_text(window)
336
- var result: Dictionary = {"windows": windows, "elements": elements}
340
+ var result: Dictionary = { "windows": windows, "elements": elements }
337
341
  if not took.is_empty():
338
342
  result["tookMsBy"] = took
339
343
  if placing.tooltips > 0:
340
344
  result["tooltip_note"] = (
341
- "tooltip is the node's tooltip_text, which the engine puts in the tree, and UI Automation, as"
342
- + " AccessKit fills it in, has no property that carries it (measured on 4.7.2), so a screen"
343
- + " reader on Windows is not told it. Say what a reader needs in accessibility_description."
345
+ "tooltip is the node's tooltip_text, which the engine puts in the tree, and UI "
346
+ + "Automation, as AccessKit fills it in, has no property that carries it (measured on "
347
+ + "4.7.2), so a screen reader on Windows is not told it. Say what a reader needs in "
348
+ + "accessibility_description."
344
349
  )
345
350
  if placing.asked > 0 and placing.placed == 0:
346
351
  # Nothing at all matched: the element ids are not the engine's element RIDs any more, which
347
- # an AccessKit that numbers its elements another way would do, and every node would read null.
352
+ # an AccessKit that numbers its elements another way would do, and every node would read
353
+ # null.
348
354
  result["node_note"] = (
349
- "No element could be matched to the node that made it: UI Automation's runtime ids did not"
350
- + " carry the engine's element ids, so node and part_of are the windows' alone."
355
+ "No element could be matched to the node that made it: UI Automation's runtime ids did "
356
+ + "not carry the engine's element ids, so node and part_of are the windows' alone."
351
357
  )
352
358
  if answer.has("description_unread"):
353
359
  result["description_note"] = (
354
- "Descriptions could not be read, so none is shown: %s" % str(answer["description_unread"])
360
+ "Descriptions could not be read, so none is shown: %s"
361
+ % str(answer["description_unread"])
355
362
  )
356
363
  if elements <= windows.size():
357
364
  var setting: String = _setting()
@@ -359,31 +366,35 @@ func get_accessibility(params: Dictionary) -> Dictionary:
359
366
  # the setting and a screen reader can say the tree is built; otherwise an empty window is
360
367
  # most likely a game that builds none.
361
368
  # 1 is a screen reader running, 0 none and -1 a platform that cannot say.
362
- var read_aloud: bool = setting == "auto" and DisplayServer.accessibility_screen_reader_active() == 1
369
+ var read_aloud: bool = (
370
+ setting == "auto" and DisplayServer.accessibility_screen_reader_active() == 1
371
+ )
363
372
  if setting != "always" and not read_aloud:
364
373
  return _refused(
365
374
  (
366
375
  (
367
- "UI Automation found nothing in the game's windows: the engine builds the tree a screen"
368
- + " reader reads only with accessibility/general/accessibility_support at Always Active,"
369
- + " or at Auto with a screen reader running when it starts, and this project's is %s."
370
- + ' Start the run with engineArgs ["--accessibility", "always"].'
376
+ "UI Automation found nothing in the game's windows: the engine builds the "
377
+ + "tree a screen reader reads only with "
378
+ + "accessibility/general/accessibility_support at Always Active, or at "
379
+ + "Auto with a screen reader running when it starts, and this project's "
380
+ + "is %s. "
381
+ + 'Start the run with engineArgs ["--accessibility", "always"].'
371
382
  )
372
383
  % setting
373
384
  )
374
385
  )
375
386
  result["_note"] = (
376
- "The windows hold nothing a screen reader is told: Controls are in the tree, and Node2D and"
377
- + " Node3D are not unless the game creates elements for them."
387
+ "The windows hold nothing a screen reader is told: Controls are in the tree, and "
388
+ + "Node2D and Node3D are not unless the game creates elements for them."
378
389
  )
379
390
  elif unnamed > 0:
380
391
  # Seen on a game naming its lines with accessibility_update_set_name: the text was there in
381
392
  # the game and empty to the screen reader.
382
393
  result["_note"] = (
383
394
  (
384
- "%d text element(s) on screen have no name, so a screen reader reads them as nothing. Static"
385
- + " text is read from its value, DisplayServer.accessibility_update_set_value, and a name"
386
- + " set on it is not read."
395
+ "%d text element(s) on screen have no name, so a screen reader reads them as "
396
+ + "nothing. Static text is read from its value, "
397
+ + "DisplayServer.accessibility_update_set_value, and a name set on it is not read."
387
398
  )
388
399
  % unnamed
389
400
  )
@@ -498,7 +509,9 @@ static func _cut(element: Dictionary, left: Array[int], omitted: Array[int]) ->
498
509
 
499
510
  ## The project's accessibility mode, as the command line's --accessibility names them.
500
511
  static func _setting() -> String:
501
- match GdhRead.as_int(ProjectSettings.get_setting("accessibility/general/accessibility_support", 0), 0):
512
+ match GdhRead.as_int(
513
+ ProjectSettings.get_setting("accessibility/general/accessibility_support", 0), 0
514
+ ):
502
515
  1:
503
516
  return "always"
504
517
  2:
@@ -576,7 +589,9 @@ static func _match(element: Dictionary, made_by: Dictionary, placing: GdhPlacing
576
589
  ## column of plain containers came out straight under the window. So the node is found where it
577
590
  ## must be: below the shown element's node, itself left out of the tree (a node that is shown would
578
591
  ## have the part under it), drawn around the part, and, for text, holding its words.
579
- static func _attribute(element: Dictionary, holder: Node, placing: GdhPlacing, is_window: bool) -> void:
592
+ static func _attribute(
593
+ element: Dictionary, holder: Node, placing: GdhPlacing, is_window: bool
594
+ ) -> void:
580
595
  var children: Array = element.get("children", [])
581
596
  for child: Variant in children:
582
597
  if not child is Dictionary:
@@ -636,7 +651,9 @@ static func _owners_of(element: Dictionary, holder: Node, placing: GdhPlacing) -
636
651
 
637
652
  ## Where a Control is drawn is read off its screen transform, which for a window that is not
638
653
  ## embedded is in that window's own pixels, as the bounds are.
639
- static func _drawn_around(node: Node, drawn: Rect2, placing: GdhPlacing, found: Array[Node]) -> void:
654
+ static func _drawn_around(
655
+ node: Node, drawn: Rect2, placing: GdhPlacing, found: Array[Node]
656
+ ) -> void:
640
657
  for child: Node in node.get_children(true):
641
658
  # A node that is shown has its parts under it, and so do the nodes below it.
642
659
  if placing.shown.has(child.get_instance_id()):
@@ -678,7 +695,7 @@ static func _text_of(node: Node) -> String:
678
695
  static func _made(parts: Array, made_by: Dictionary) -> Node:
679
696
  var halves: Array[int] = []
680
697
  for part: Variant in parts:
681
- halves.append(GdhRead.as_int(part, 0) & 0xFFFFFFFF)
698
+ halves.append(GdhRead.as_int(part, 0) & 0xffffffff)
682
699
  for at: int in [0, 2]:
683
700
  var id: int = (halves[at] << 32) | halves[at + 1]
684
701
  if made_by.has(id):
@@ -686,21 +703,15 @@ static func _made(parts: Array, made_by: Dictionary) -> Node:
686
703
  return null
687
704
 
688
705
 
689
- ## Counts kept while the elements are placed, and the nodes whose own elements UI Automation shows,
690
- ## by instance id.
691
- class GdhPlacing:
692
- extends RefCounted
693
- var asked: int = 0
694
- var placed: int = 0
695
- var tooltips: int = 0
696
- var shown: Dictionary = {}
697
-
698
-
699
706
  ## Text elements with bounds and no name. The window's announcement element has no bounds and is
700
707
  ## left out with them: it is empty until something is announced.
701
708
  static func _unnamed_text(element: Dictionary) -> int:
702
709
  var total: int = 0
703
- if element.get("role") == "text" and str(element.get("name", "")).is_empty() and element.has("bounds"):
710
+ if (
711
+ element.get("role") == "text"
712
+ and str(element.get("name", "")).is_empty()
713
+ and element.has("bounds")
714
+ ):
704
715
  total += 1
705
716
  var children: Array = element.get("children", [])
706
717
  for child: Variant in children:
@@ -710,8 +721,8 @@ static func _unnamed_text(element: Dictionary) -> int:
710
721
  return total
711
722
 
712
723
 
713
- ## When a client started at [param began_at] is stopped, in ticks: [constant START_MS] after it began
714
- ## while UI Automation is loading ([param loaded_at] -1), and once it was seen loaded at
724
+ ## When a client started at [param began_at] is stopped, in ticks: [constant START_MS] after it
725
+ ## began while UI Automation is loading ([param loaded_at] -1), and once it was seen loaded at
715
726
  ## [param loaded_at], [constant SETTLE_MS] for each of [param windows] and [constant WALK_SLACK_MS]
716
727
  ## after that.
717
728
  static func _deadline(began_at: int, loaded_at: int, windows: int) -> int:
@@ -763,8 +774,8 @@ static func _reached(progress: String, started_ms: int) -> String:
763
774
  )
764
775
  return (
765
776
  (
766
- "PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the windows"
767
- + " were walked %d time%s, the last finding %s element%s"
777
+ "PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the "
778
+ + "windows were walked %d time%s, the last finding %s element%s"
768
779
  )
769
780
  % [began, loaded, walks, "" if walks == 1 else "s", last, "" if last == "1" else "s"]
770
781
  )
@@ -789,4 +800,14 @@ static func _took(progress: String, started_ms: int, done_ms: int) -> Dictionary
789
800
 
790
801
 
791
802
  static func _refused(message: String) -> Dictionary:
792
- return {"type": "error", "message": message}
803
+ return { "type": "error", "message": message }
804
+
805
+
806
+ ## Counts kept while the elements are placed, and the nodes whose own elements UI Automation shows,
807
+ ## by instance id.
808
+ class GdhPlacing:
809
+ extends RefCounted
810
+ var asked: int = 0
811
+ var placed: int = 0
812
+ var tooltips: int = 0
813
+ var shown: Dictionary = {}