# Avalonia, тесты и сборка: уже оплаченные грабли Читать перед правкой `.axaml`, стилей, тестовых проектов и файлов сборки. ## XAML и стили - **Селектор типа матчит точный тип.** `UserControl.shell` не матчит `ShellView` (наследник `ReactiveUserControl`) и молча не делает ничего — нужен `:is(UserControl).shell`. Голый `.shell` тоже матчит, но тогда XAML-компилятор не выводит тип для `Setter` и падает с AVLN2200. - **Style-сеттер навсегда проигрывает локальному значению.** Не стилизовать `IsPaneOpen` и `DisplayMode` — они биндятся во ViewModel. - **`IPseudoClasses.Set` требует ведущего `:`.** - **Compiled bindings включены по умолчанию** (Avalonia 12): `x:DataType` нужен на каждом `UserControl` и каждом `DataTemplate`. - **`{DynamicResource}` с несуществующим ключом молча не срабатывает** — свойство остаётся со значением по умолчанию, фон не красится, кисть прозрачная. Дважды доезжало до скриншота; ловит `ResourceKeyTests` в обеих темах. - **`{l:Loc}` с ключом, которого нет в обоих resx, тест паритета не поймает** — файлы согласованы между собой. Ловит `LocalizationCoverageTests`. ## Приложение - **`InvariantGlobalization` обязан быть `false`**: Semi.Avalonia строит `CultureInfo` в статическом конструкторе и падает целиком. - **Превьюер рефлексирует безпараметровый статический `BuildAvaloniaApp()`**: необязательный параметр ломает его вызов, вторая перегрузка — `AmbiguousMatchException`. - **Инспектора в Avalonia 12 нет из коробки.** `Avalonia.Diagnostics` закончился на 11.3.x; DevTools живут отдельно (`AvaloniaUI.DiagnosticsSupport` + `.WithDeveloperTools()`), со своей установкой. Зависимость намеренно не добавлена. ## Тесты - **`Avalonia.Headless.XUnit` 12.x — это xUnit v3**, а не v2. Тестовые проекты — `Exe`. - **Headless: ручной `Measure`/`Arrange` внутри окна бесполезен** — следующий проход layout окна вернёт свой размер. Задавать ширину самому `Window`. Но и без окна нельзя: у открепленного контрола не строится визуальное дерево. - **`Execute()` завершился ≠ `IsExecuting` уже false.** Второе публикуется на выходном планировщике. Тест, который сразу после `await` дёргает команду, закрытую по чужому `IsExecuting`, будет мигать под нагрузкой — ждите `CanExecute`, а не предполагайте. - **Проект VM-тестов не параллелится**: `ReactiveUiBootstrap` ставит глобальные планировщики ReactiveUI, то есть тесты делят изменяемое состояние независимо от их желания. ## Композиция - **`Dispose()` страницы обязан переживать второй вызов.** Каждая страница зарегистрирована дважды — своим типом и как `PageViewModel`, — и контейнер учитывает её как disposable по обеим регистрациям, то есть зовёт `Dispose()` два раза. `GalleryViewModel` отменял на втором вызове уже освобождённый `CancellationTokenSource`, и приложение падало с `ObjectDisposedException` при каждом выходе. Ловят тесты `Disposing_twice_is_safe`. ## Сборка - **csharpier — единственный владелец форматирования** (включая `.axaml` и `.csproj`). `IDE0055` понижен до suggestion: два форматтера с `TreatWarningsAsErrors` дерутся насмерть. - **`Condition="'$(Configuration)' == 'Debug'"` на `PackageReference` — ловушка.** `dotnet restore` вычисляется с конфигурацией по умолчанию, поэтому пакет обязан резолвиться, даже если эту конфигурацию никто не собирает. Так тут проехал мёртвый `Avalonia.Diagnostics`: `dotnet build -c Release` работал, а голый `dotnet restore` падал. - Версии пакетов централизованы в `Directory.Packages.props` — **никаких `Version=` в csproj** (иначе NU1008 на restore). - `TreatWarningsAsErrors` включён; NuGet-advisory (`NU19xx`) выведены из ошибок, чтобы свежая CVE не роняла сборку кода, который никто не трогал. - Тестовые послабления анализаторов — в `tests/Directory.Build.props`, не в самих тестах.