К содержимому

SusAudit — встроенные проверки (Debug / QA)

Автоматические модули аудита, встроенные в sus-core и sus-router. Все они включаются через #if UNITY_EDITOR || DEVELOPMENT_BUILD. В release-сборке компилятор полностью вырезает их — нулевые накладные расходы в рантайме.

SUS Inspector Editor window with Overview, Inspect, Health, Compile, Connect, and Settings tabs open on the element tree

Сводная таблица

#МодульГде встроенТриггерАвтоматически
1ClickAuditSusComponent + ClickAuditServiceКаждый ClickEvent на зарегистрированных элементах
2BoundsAuditконструктор SusComponent2 кадра после mount
3CallbackAuditметоды SusComponent.cs + 16 .sharqусловия guard / тайминг в обработчике ClickEvent⚠️ .sharq
4OverlayAuditOverlayHost.AddToOverlay()Добавление элемента в overlay
5StateAudit.sharq (SusButton/Toggle/Dropdown/ListGroup/Modal)WatchEffect на противоречивых props⚠️ .sharq
6LifecycleAuditSusComponent.OnDetachFromPanelHandler()Detach от panel
7NavigationAuditSusRouter.Push/Replace/PushNamed/ReplaceNamedResolve() вернул null
8PerformanceAuditконструктор SusComponent500 мс после mount
9DebounceAuditконструктор SusComponentКаждый ClickEvent на интерактивных элементах
10ClickTargetSizeAuditконструктор SusComponent (вместе с BoundsAudit)2 кадра после mount
11StackDepthAuditSusRouter.PushRecordПосле _history.Add()
12GuardAuditSusRouter.Navigate()После NavigateCore — если результат Aborted
13ModalStackAuditOverlayHost.AddToOverlay()Добавление модалки в overlay
14EmptyStateAudit.sharq (SusListGroup/SusDropdown)Items.Count == 0, но элемент открыт/виден⚠️ .sharq
15RemountLoopAuditSusComponent.OnAttachToPanelHandler()>5 Attach за 1 секунду
16OverflowAuditконструктор SusComponentДети выходят за границы родителя (Unity не делает clip)
17DeadRouteAuditSusRouter (ручной вызов AuditUnusedRoutes())Зарегистрированные, но никогда не использованные маршруты🔧 вручную
18SusTable StateAudit.sharq (SusTable)WatchEffectItemsPerPage > Items или Page > TotalPages⚠️ .sharq
19LayoutReentryAuditSusComponent.OnGeometryChangedForBreakpoint()>20 GeometryChanged за 500 мс
20IdleGuardAuditконструктор SusComponentЭлемент виден 30+ секунд без кликов
21FocusTrapAudit.sharq (SusModal)FocusEvent — Tab уводит фокус из модалки⚠️ .sharq

✅ = полностью автоматически, ничего добавлять в компоненты не нужно. ⚠️ = требует одной строки SetClickAuditDescription("Name") или WatchEffect в .sharq (уже встроено во все компоненты). 🔧 = ручной вызов метода.


1. ClickAudit — доходит ли клик до элемента

Файлы: sus-core/Runtime/Diagnostics/ClickAuditService.cs, sus-core/Runtime/SusComponent.cs

Проблема: элемент зарегистрирован как кликабельный, но при клике мышью событие до него не доходит — его перехватывает другой элемент сверху (tooltip, overlay, модалка с неправильным pickingMode).

Как это работает:

  • ClickAuditService.Instance.Install(panel) — вызывается автоматически при первом SusBootstrap.Mount<T>().
  • Сервис регистрирует глобальный ClickEvent на panel.visualTree.
  • При каждом клике сравнивает evt.target с зарегистрированными элементами.
  • Если клик не попал ни в один зарегистрированный элемент, логируется слой перехватившего элемента.

Пример предупреждения:

[ClickAudit] Active: 'SusButton' blocked at center. Reason: Covered by 'SusTooltip'

Интеграция: элемент регистрируется через SetClickAuditDescription("Name") в Created(). Уже встроено в 50 downstream-компонентов (kit 40 + game 10) .


2. BoundsAudit — ненулевые ли размеры

Файл: sus-core/Runtime/SusComponent.cs (конструктор)

Проблема: Unity UITK не вызывает ContainsPoint для элементов с нулевыми размерами. Клик проходит насквозь. Причины:

  • Нет flexGrow, width, height в USS/стилях
  • Родитель не задал размер
  • display: none / visible: false
  • Элемент не добавлен в дерево (не attached к panel)

Как это работает: отложенная проверка через schedule.Execute(...).StartingIn(150) — ждёт 2-3 кадра, пока посчитается layout, затем проверяет worldBound.width и worldBound.height.

Пример предупреждения:

[BoundsAudit] 'SusUnitCard' has zero bounds after mount (0×0).
  display=Flex, visible=True, pickingMode=Position.
  Clicks will NOT reach this element.

3. CallbackAudit — сработал ли OnClick

Файлы: sus-core/Runtime/SusComponent.cs, 16 .sharq

Проблема: ClickEvent зарегистрирован, элемент доступен, клик проходит — но обработчик НЕ вызывается. Причины:

  • Disabled.Value = true → обработчик выходит раньше времени
  • Loading.Value = true → обработчик выходит раньше времени
  • Guard-условие: if (someCondition) return;
  • Обработчик выполнился, но занял > 50 мс (подозрительно долго)

Два подрежима:

3a. Guard-lock (AuditClickBlocked)

csharp
// In SusButton.sharq:
if (Disabled.Value)
{
    AuditClickBlocked("Disabled");  // ← warning to the console
    return;
}

Пример: [CallbackAudit] 'SusButton' click blocked: Disabled

3b. Тайминг (AuditClickStart / AuditClickEnd)

csharp
var t0 = AuditClickStart();
OnClick?.Invoke();
AuditClickEnd(t0);  // if > 50ms → warning

Пример: [CallbackAudit] 'SusButton' OnClick took 127.3ms

Встроено в (16 .sharq): SusAlert, SusButton, SusDropdown, SusEmptyState, SusForm, SusLink, SusListGroup, SusMenuButton, SusNumberInput, SusPagination, SusRating, SusSnackbar, SusStepper, SusTabs, SusToggle, SusUnitCard.


4. OverlayAudit — перекрытие UI

Файл: sus-core/Runtime/OverlayHost.cs

Проблема: при добавлении элемента в OverlayHost (tooltip, dropdown, модалка) новый элемент может перехватывать клики своими pickable-детьми.

Как это работает: в AddToOverlay() после вставки элемента Query<VisualElement>() ищет внутри всё с pickingMode == Position, и если такие находятся (а категория не Modal) — выводится предупреждение.

Пример предупреждения:

[OverlayAudit] 'SusTooltip' added to overlay in Tooltip category
  with 3 pickable children. It may block clicks to underlying UI.
  Consider setting pickingMode=Ignore on children.

5. StateAudit — консистентность props

Файлы: SusButton.sharq, SusToggle.sharq, SusDropdown.sharq, SusListGroup.sharq, SusModal.sharq

Проблема: несогласованные состояния — Disabled=true и Loading=true одновременно (спиннер никогда не появится), IsOpen=true при Disabled=true (dropdown открыт, но неактивен), Selected не входит в Items (нет выделенного элемента), Model=false, но модалка видна.

Как это работает: WatchEffect внутри .sharq-компонентов следит за комбинациями props.

КомпонентПравило
SusButtonDisabled && Loading → оба true одновременно
SusToggleDisabled && Loading → оба true одновременно
SusDropdownIsOpen && Disabled → открыт, но disabled
SusListGroupSelected не входит в Items → нет выделенного элемента
SusModalModel=false, но display ≠ None → скрытая модалка видна

Примеры предупреждений:

[StateAudit] SusButton: both Disabled and Loading are true.
  Loading spinner will never be visible.

[StateAudit] SusDropdown: IsOpen=true while Disabled=true.
  Dropdown should close when disabled.

[StateAudit] SusListGroup: Selected value is not in Items.
  List will have no highlighted item.

[StateAudit] SusModal: Model=false but element is still visible
  (display=Flex). Modal should be hidden.

6. LifecycleAudit — утечки подписок

Файл: sus-core/Runtime/SusComponent.cs (OnDetachFromPanelHandler)

Проблема: элемент удалён из panel, но подписки на Prop<T>.Changed остались — утечка памяти и потенциальные ошибки при remount.

Как это работает: при detach запоминается _bindings.Count, через 1 секунду проверяется, что panel всё ещё null, а число подписок не уменьшилось → утечка.

Пример предупреждения:

[LifecycleAudit] 'SusUnitCard' detached but 3 bindings were not disposed.
  Possible leak.

7. NavigationAudit — неразрешённые маршруты

Файл: sus-router/Runtime/SusRouter.cs

Проблема: вызван SusRouter.Push("/settings"), но путь не разрешился (Resolve вернул null) — пользователь нажимает кнопку, ничего не происходит.

Как это работает: в каждом методе навигации (Push, Replace, PushNamed, ReplaceNamed) — если record == null → предупреждение.

Пример предупреждения:

[NavigationAudit] Push('/battle/unknown') — route not found.

8. PerformanceAudit — слишком много элементов

Файл: sus-core/Runtime/SusComponent.cs (конструктор)

Проблема: глубокое дерево или много детей (>500 VisualElements) → медленный PickAll, долгий layout, тормозящий UI.

Как это работает: через 500 мс после mount Query<VisualElement>() считает все элементы в поддереве. Если > 500 → предупреждение.

Пример предупреждения:

[PerfAudit] 'SusTable' has 1423 VisualElements.
  Consider virtualization (SusTable) or paging.

9. DebounceAudit — двойные клики

Файл: sus-core/Runtime/SusComponent.cs (конструктор)

Проблема: пользователь быстро нажимает кнопку дважды (< 300 мс между кликами) — возможен double-submit (два API-вызова, двойное списание валюты и т.д.).

Как это работает: в конструкторе SusComponent регистрируется callback ClickEvent (срабатывает ДО обработчиков в Created() — UITK гарантирует порядок регистрации). Для каждого интерактивного элемента (с SetClickAuditDescription) запоминается время последнего клика. Если прошло <300 мс — предупреждение.

Пример предупреждения:

[DebounceAudit] 'SusButton' rapid double-click (87ms).
  Possible unintended double-submit.

Важно: это аудит, а не блокировка. Он не предотвращает второй клик — только предупреждает разработчика о необходимости debounce в бизнес-логике.


10. ClickTargetSizeAudit — маленькая цель

Файл: sus-core/Runtime/SusComponent.cs (конструктор, вместе с BoundsAudit)

Проблема: интерактивный элемент (кнопка, иконка, чекбокс) меньше 30×30px — трудно попасть пальцем/мышью. HIG рекомендует минимум 44×44px.

Как это работает: вместе с BoundsAudit, для элементов с SetClickAuditDescription — если worldBound > 0, но < 30px по любой оси → предупреждение.

Пример предупреждения:

[ClickTargetAudit] 'SusIcon' tap target is small (16×16px).
  HIG recommends ≥44×44. Consider padding or min-size.

11. StackDepthAudit — глубина навигационного стека

Файл: sus-router/Runtime/SusRouter.cs

Проблема: история навигации растёт неограниченно (> 50 записей) — возможна циклическая навигация (A → B → A → B...) или забытый Replace() вместо Push().

Как это работает: после каждого _history.Add() в PushRecord проверяется _history.Count > 50.

Пример предупреждения:

[StackDepthAudit] Router history has 67 entries.
  Possible circular navigation or unbounded stack growth.
  Consider using Replace() instead of Push().

12. GuardAudit — отклонённая навигация

Файл: sus-router/Runtime/SusRouter.cs

Проблема: пользователь нажимает кнопку перехода, навигация молча отклоняется guard'ом или lifecycle-хуком — экран не меняется, и сообщений об ошибке нет.

Как это работает: в Navigate() — единая точка перехвата после NavigateCore. Если результат NavigationResult.Aborted — предупреждение с путями from→to.

Пример предупреждения:

[GuardAudit] Nav from '/lobby' → '/battle' was rejected by a guard
  or lifecycle hook (BeforeLeave/CanLeave/BeforeEach/CanEnter/BeforeResolve/BeforeEnter).

Покрывает все точки прерывания: BeforeRouteUpdate, BeforeLeave, CanLeave (ISusRouteGuard), BeforeEach (глобальный), CanEnter (ISusRouteGuard), BeforeResolve (глобальный), BeforeEnter.


13. ModalStackAudit — слишком много модалок

Файл: sus-core/Runtime/OverlayHost.cs

Проблема: на экране одновременно > 5 модалок — возможен баг (незакрытые модалки) или плохой UX (пользователь не понимает, какая модалка активна).

Как это работает: в AddToOverlay() после добавления считается _stack.Count(e => e.Category == OverlayCategory.Modal). Если > 5 — предупреждение.

Пример предупреждения:

[ModalStackAudit] 7 modals on screen.
  Deep modal stacking may indicate a flow bug or unclosed modals.

Архитектура

sus-core/Runtime/
├── SusComponent.cs            ← BoundsAudit, ClickTargetAudit, PerfAudit,
│                                 DebounceAudit, LifecycleAudit, RemountLoopAudit,
│                                 OverflowAudit, IdleGuardAudit, LayoutReentryAudit,
│                                 AuditClickBlocked/Start/End
├── Diagnostics/
│ └── ClickAuditService.cs ← ClickAudit (global click interception)
├── OverlayHost.cs             ← OverlayAudit, ModalStackAudit

sus-router/Runtime/
└── SusRouter.cs               ← NavigationAudit, GuardAudit, StackDepthAudit,
                                  DeadRouteAudit

Downstream UI packages may register additional component-level audits
(CallbackAudit, StateAudit, EmptyStateAudit, FocusTrapAudit, …).

Включение / выключение

Все модули активны, когда:

csharp
#if UNITY_EDITOR || DEVELOPMENT_BUILD

В release-сборке (!UNITY_EDITOR && !DEVELOPMENT_BUILD) код полностью вырезается компилятором — ни единого байта, ни единого вызова.


14. EmptyStateAudit — пустой список / dropdown

Файлы: SusListGroup.sharq, SusDropdown.sharq

Проблема: Items.Count == 0, но элемент открыт или виден — пользователь видит пустой контейнер.

Как это работает: WatchEffect внутри .sharq следит за этой комбинацией.

Примеры предупреждений:

[EmptyStateAudit] SusListGroup: Items is empty but element is visible.
[EmptyStateAudit] SusDropdown: IsOpen=true but Items is empty.

15. RemountLoopAudit — циклические remount'ы

Файл: sus-core/Runtime/SusComponent.cs (OnAttachToPanelHandler)

Проблема: > 5 Attach за 1 секунду → баг реактивности (WatchEffect меняет visibility/layout).

Пример предупреждения:

[RemountLoopAudit] 'SusUnitCard' attached 7 times in 1s.

16. OverflowAudit — дети выходят за границы

Файл: sus-core/Runtime/SusComponent.cs (конструктор)

Проблема: дети выходят за границы родителя. Unity UITK не поддерживает overflow: hidden.

Пример предупреждения:

[OverflowAudit] 'SusCard' has 3 child(ren) exceeding parent bounds (320×180).

17. DeadRouteAudit — неиспользуемые маршруты

Файл: sus-router/Runtime/SusRouter.cs

Проблема: зарегистрированные, но никогда не используемые маршруты — мёртвый код.

Вызов: router.AuditUnusedRoutes() (вручную, из dev-панели или тестов).

Пример предупреждения:

[DeadRouteAudit] 3 registered routes were never navigated to:
  - /settings/audio (settings-audio)

18. SusTable StateAudit — несогласованная пагинация

Файл: SusTable.sharq

Проблема: ItemsPerPage > Items.Count — одна страница, лишняя пагинация. Page > TotalPages — таблица показывает пустые строки.

Как это работает: WatchEffect следит за Controller.Value.Items, ItemsPerPage, Page, TotalPages.

Примеры предупреждений:

[StateAudit] SusTable: ItemsPerPage=25 but only 3 items. Single page.
[StateAudit] SusTable: Page=5 exceeds TotalPages=3. Table may show empty rows.

19. LayoutReentryAudit — рекурсивный layout

Файл: sus-core/Runtime/SusComponent.cs (OnGeometryChangedForBreakpoint)

Проблема: WatchEffect меняет размер/позицию → GeometryChangedEventUpdated()WatchEffect → бесконечный цикл. UI зависает с высокой нагрузкой на CPU.

Как это работает: скользящее окно 500 мс, счётчик GeometryChangedEvent. Если > 20 за окно — предупреждение.

Пример предупреждения:

[LayoutReentryAudit] 'SusCard' 47 geometry changes in 500ms.

20. IdleGuardAudit — по элементу ни разу не кликнули

Файл: sus-core/Runtime/SusComponent.cs (конструктор)

Проблема: интерактивный элемент виден 30+ секунд, но ни одного клика — возможно, pickingMode=Ignore, элемент перекрыт прозрачным overlay или забыт ClickEvent.

Как это работает: одноразовая проверка через 30 секунд после mount. Если _lastClickTime == 0 — предупреждение с диагностической информацией (pickingMode, worldBound).

Пример предупреждения:

[IdleGuardAudit] 'SusButton' visible but never clicked.
  pickingMode=Ignore, worldBound=(320,180 80×32).

21. FocusTrapAudit — фокус вне модалки

Файл: SusModal.sharq

Проблема: когда модалка открыта, Tab уводит фокус на элементы позади неё — нарушение accessibility.

Как это работает: FocusEvent + this.Contains(focused). Если Model=true, а evt.target не внутри модалки — предупреждение.

Пример предупреждения:

[FocusTrapAudit] SusModal: focus escaped outside modal to 'TextField'.

Связанные документы

22. ScreenAudit — текстовые дампы структуры экрана

Файл: sus-core/Runtime/Diagnostics/ScreenAudit.cs

Проблема: нужно понять «что видит пользователь на экране?» без запуска Unity. AI-агент не может посмотреть на экран — ему нужен текстовый отчёт.

Как это работает: три режима текстового дампа, вывод в Debug.Log (читается через read_console MCP).

22.1 LayoutDump — карта всех элементов с координатами

══════════ LayoutDump ══════════
Root: TemplateContainer  panel=True
Screen: 1920×1080

│SusApp ⚙ 🖱 [3ch] flex (0,0 1920×1080)
│  classes: .sus-app .theme-dark
│  📍clickable area: (0,0)→(1920,1080)
││SusMainMenu ⚙ 🖱 [1ch] flex (0,0 900×1080)
││  📍clickable area: (0,0)→(900,1080)
│││SusButton ⚙ 🖱 [1ch] flex (300,200 200×40)
│││  📍clickable area: (300,200)→(500,240)
││SusSettings ⚙ ⊘ HIDDEN [0ch] none (0,0 0×0)

Расшифровка иконок:

  • = SusComponent, 🖱 = кликабельно, = прозрачно для кликов
  • HIDDEN = не видим (display: none / visible=false / нулевой размер)
  • 📍clickable area = фактическая кликабельная область

22.2 PickableLayerAudit — z-order кликабельных элементов

══════ PickableLayerAudit ══════
Z-order = DOM order (last sibling = topmost). No z-index in Unity.

[LAYER 000] SusMainMenu ⚙ ⚠OVERLAPPED
  area: (0,0 900×1080)
  picking=Position visible=True enabled=True
  ← blocked by [001]SusOverlayPanel
[LAYER 001] SusOverlayPanel ⚙
  area: (0,0 900×1080)
  picking=Position visible=True enabled=True

⚠ 2 elements have overlapping bounds with higher z-order elements.

Чтение: [LAYER 000] — чем больше число, тем выше (ближе к глазам). ⚠OVERLAPPED — кто-то выше по z-order перекрывает этот элемент: клик уйдёт наверх.

22.3 FullPropsDump — все значения Prop всех SusComponent'ов

══════ FullPropsDump ══════
── SusMainMenu #main-menu (900×1080)
  IsOpen = true
  Mode = "deployment"
── SusButton #play-btn (200×40)
  Text = "PLAY"
  Disabled = false
  Loading = false
  Variant = "primary"
Total: 2 SusComponents dumped.

Горячая клавиша

Ctrl+Shift+~ — сразу три дампа в консоль. Устанавливается автоматически через SusBootstrap.

Автоматический дамп

При каждой навигации роутера (Push, Replace, PushNamed, ReplaceNamed, Back, Forward, Go) — после успешного перехода автоматически выводятся:

  • LayoutDump — карта элементов на новом экране
  • FullPropsDump — значения всех Prop всех SusComponent'ов

При каждом клике по SusButton — после выполнения OnClick?.Invoke() автоматически выводится FullPropsDump. Это даёт агенту снапшот состояния UI сразу после завершения действия кнопки.

Оба механизма — под #if UNITY_EDITOR || DEVELOPMENT_BUILD, полностью вырезаются в release.

Использование агентом

# Via MCP: read Unity console
read_console -> filter_text="[LA]" # LayoutDump (element map)
read_console -> filter_text="[PA]" # PickableLayer (z-order)
read_console -> filter_text="[FP]" # FullPropsDump (props values)

Агент получает текстовую картину экрана и может:

  • Понять структуру: какие компоненты, в каком порядке
  • Найти перекрытия: какие элементы блокируют клик
  • Увидеть состояние: текущие значения props всех компонентов