-
Notifications
You must be signed in to change notification settings - Fork 0
Parley Developer Testing
Guide for writing and running tests in Parley.
- Overview
- Running Tests
- Test Types
- Controller Testing
- Testing with DI and Mocks
- Writing New Tests
- Common Patterns
- Privacy and Security
- Debugging Tests
- CI/CD Integration
- Best Practices
Parley uses xUnit for unit testing with Avalonia.Headless for GUI integration tests.
Current Coverage (as of 2026-06-07):
- 900 tests (last verified in PR #2388 pre-merge sweep)
- Parser/GFF, Copy/Paste, Delete, Undo/Redo, Orphan Detection
- Controller unit tests (5 controller test classes)
- ScrapSerializationService tests (round-trip, persistence, migration)
- ScrapRestoreService tests (single/batch/subtree restore, validation)
- GameResourceService tests (graceful degradation, TLK handling, disposal)
- SoundFilterService tests (mono filter, text search, combined filters, edge cases)
- Plugin Security (sandbox, permissions, rate limiting)
- Settings, Logging, Script Services
- Flowchart conversion + FlowView/TreeView sync, reparent, panel viewmodel
- Drag/Drop validation (parity between TreeView and FlowView, validator unit tests)
- Tree management (refresh coordinator, navigation, selection, safe-node binding)
- Stress and edge-case tests (cascade delete, lazy loading, corruption, real-world round-trip)
- Property-panel integration tests (quest, script, speaker)
- GUI Headless tests
cd Parley
dotnet testdotnet test --filter "FullyQualifiedName~DialogLoadingHeadlessTests"dotnet test --filter "FullyQualifiedName=Parley.Tests.GUI.DialogLoadingHeadlessTests.CreateDialog_InitializesCorrectly"dotnet test --logger "console;verbosity=detailed"When to use: Testing business logic, data models, services
Attribute: [Fact] or [Theory]
[Fact]
public void SanitizePath_ReplacesUserProfileWithTilde()
{
// Arrange
var testPath = Path.Combine(_testUserProfile, "Parley", "Logs", "test.log");
// Act
var sanitized = UnifiedLogger.SanitizePath(testPath);
// Assert
Assert.StartsWith("~", sanitized);
Assert.DoesNotContain(_testUserProfile, sanitized);
}When to use: Testing UI workflows, dialog loading, node creation
Attribute: [AvaloniaFact] or [AvaloniaTheory]
[AvaloniaFact]
public void CreateDialog_InitializesCorrectly()
{
var dialog = new Dialog();
Assert.NotNull(dialog);
Assert.NotNull(dialog.Entries);
Assert.Empty(dialog.Entries);
}Key Differences:
- Use
[AvaloniaFact]instead of[Fact] - Tests run in headless mode (no actual UI window)
- Avalonia application initialized automatically
When to use: Testing file I/O, async workflows
[AvaloniaFact]
public async Task LoadDialog_SimpleFile_ParsesSuccessfully()
{
var testFile = Path.Combine(_testFilesPath, "test1_link.dlg");
var dialogService = new DialogFileService();
var dialog = await dialogService.LoadFromFileAsync(testFile);
Assert.NotNull(dialog);
Assert.NotEmpty(dialog.Entries);
}Controllers extracted during the Sprint 1-4 refactor have dedicated unit tests in Parley.Tests/Controllers/.
| Test Class | Controller Under Test |
|---|---|
| EditMenuControllerTests | EditMenuController |
| FileMenuControllerTests | FileMenuController |
| QuestUIControllerTests | QuestUIController |
| SpeakerVisualControllerTests | SpeakerVisualController |
| TreeViewUIControllerTests | TreeViewUIController |
Controllers accept dependencies via constructor, making them testable with mocks:
[Fact]
public void Constructor_ValidArgs_CreatesInstance()
{
// Arrange
var mockWindow = new MockWindow();
var mockSettings = new MockSettingsService();
// Act
var controller = new SpeakerVisualController(
mockWindow,
mockSettings,
() => false); // isPopulatingProperties callback
// Assert
Assert.NotNull(controller);
}- Constructor validation (valid args, null args)
- Method behavior with mock dependencies
- State changes from UI interactions
- Edge cases (no TreeView control, null selection, etc.)
Mock implementations exist for DI-registered service interfaces, enabling controller and integration testing without real services:
| Mock | Interface |
|---|---|
| MockSettingsService | ISettingsService |
| MockDialogContextService | IDialogContextService |
| MockScriptService | IScriptService |
| MockPortraitService | IPortraitService |
| MockJournalService | IJournalService |
| MockGameDataService | IGameDataService |
| MockImageService | IImageService |
| MockTtsService | ITtsService |
Mock services implement the interface with minimal behavior:
public class MockSettingsService : ISettingsService
{
public string ModulePath { get; set; } = "";
public string BaseGamePath { get; set; } = "";
// ... minimal property implementations
}For integration tests that need the full DI container:
[Fact]
public void ServiceResolution_AllCoreServices_Resolve()
{
var services = new ServiceCollection();
services.AddSingleton<ISettingsService>(new MockSettingsService());
// ... register test services
var provider = services.BuildServiceProvider();
var settings = provider.GetRequiredService<ISettingsService>();
Assert.NotNull(settings);
}| Scenario | Test Type | Attribute |
|---|---|---|
| Business logic | Unit Test | [Fact] |
| Controller behavior | Unit Test | [Fact] |
| Dialog loading | Headless Test | [AvaloniaFact] |
| File operations | Async Test |
[Fact] + async Task
|
| UI interaction | Headless Test | [AvaloniaFact] |
Location:
-
Parley.Tests/- Standard service/model tests -
Parley.Tests/Controllers/- Controller tests -
Parley.Tests/GUI/- Headless tests -
Parley.Tests/Integration/- Property-panel integration tests -
Parley.Tests/Services/- Service-layer focused tests -
Parley.Tests/Mocks/- Mock service implementations
Plugin Security tests live alongside the standard tests (no dedicated Security/ folder); search for Security in test class names.
Naming Convention:
- Class:
<Feature>Tests.cs - Method:
<Scenario>_<Expected>
using Avalonia.Headless.XUnit;
using DialogEditor.Models;
using Xunit;
namespace Parley.Tests.GUI
{
public class MyFeatureHeadlessTests
{
[AvaloniaFact]
public void Scenario_ExpectedBehavior()
{
// Arrange
// Act
// Assert
}
}
}private Dialog CreateSimpleDialog()
{
var dialog = new Dialog();
var entry = dialog.CreateNode(DialogNodeType.Entry);
entry!.Text.Add(0, "Test Entry");
dialog.AddNodeInternal(entry, DialogNodeType.Entry);
var entryPtr = dialog.CreatePtr();
entryPtr!.Type = DialogNodeType.Entry;
entryPtr.Node = entry;
entryPtr.Index = 0;
dialog.Starts.Add(entryPtr);
return dialog;
}public class MyTests : IDisposable
{
private readonly string _testDirectory;
public MyTests()
{
_testDirectory = Path.Combine(
Path.GetTempPath(),
$"ParleyTests_{Guid.NewGuid()}"
);
Directory.CreateDirectory(_testDirectory);
}
public void Dispose()
{
if (Directory.Exists(_testDirectory))
{
try { Directory.Delete(_testDirectory, true); }
catch { /* Ignore cleanup errors */ }
}
}
}[AvaloniaFact]
public async Task LoadDialog_File_ParsesCorrectly()
{
var testFile = Path.Combine(_testFilesPath, "test.dlg");
if (!File.Exists(testFile))
return; // Skip if test file missing
var dialog = await dialogService.LoadFromFileAsync(testFile);
Assert.NotNull(dialog);
}// Bad
[InlineData("C:\\Users\\RealName\\Documents\\file.txt")]
// Good
[InlineData("~\\Documents\\file.txt")]var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var testPath = Path.Combine(userProfile, "Parley", "test.dlg");Assert.StartsWith("~", sanitized);
Assert.DoesNotContain(userProfile, sanitized);- Set breakpoint in test method
- Right-click test in Test Explorer
- Select "Debug Test"
dotnet test --logger "console;verbosity=detailed"TypeLoadException in Headless tests
- Ensure
Avalonia.Headless.XUnitversion matches main Avalonia version
Test file not found
- Use robust path resolution with
Directory.GetCurrentDirectory() - Or skip test if file missing
Tests run automatically on PR creation via GitHub Actions:
- name: Run Tests
run: dotnet test --logger "console;verbosity=minimal"Requirements:
- All tests must pass before PR merge
- Test execution time < 30s for full suite
- No flaky tests (tests that randomly fail)
- Write tests before fixing bugs (TDD for bug fixes)
- Keep tests fast (< 2s per test)
- Use descriptive test names
- Test edge cases and error conditions
- Clean up test data in
Dispose() - Use Arrange/Act/Assert pattern
- Use mock services for controller tests
- Test controllers independently of UI
- Test implementation details (test behavior, not internals)
- Use real user paths/names in test data
- Create dependencies between tests
- Skip assertions (every test must assert something)
- Test framework code (test your logic, not xUnit/Avalonia)
- Instantiate real services when mocks suffice
| Category | Files | Description |
|---|---|---|
| Parser/GFF |
GffParserTests, BasicParserTests
|
Binary format parsing |
| Copy/Paste |
CopyPasteTests, DialogClipboardServiceTests
|
Clipboard operations |
| Delete |
DeleteOperationTests, DeleteDeepTreeTests, CascadeDeleteStressTests
|
Node deletion + stress |
| Orphan |
OrphanNodeTests, OrphanNodeCleanupTests
|
Orphan detection |
| Undo/Redo |
UndoRedoTests, UndoStackLimitTests
|
State management |
| Controllers |
Controllers/*ControllerTests (5 files) |
UI controller logic |
| Scrap |
ScrapSerializationServiceTests, ScrapRestoreServiceTests
|
Scrap persistence, restoration |
| Game Resources | GameResourceServiceTests |
Resource resolution, TLK, disposal |
| Security | Tests with Security in class name |
Plugin sandbox, permissions, rate limiting |
| Flowchart |
Flowchart*Tests, DialogToFlowchartConverterTests, FlowViewTreeViewSyncTests, FlowchartReparentTests, FlowchartPanelViewModelTests, FlowchartNodeWidthViewModelTests
|
Visual conversion + sync |
| Drag/Drop |
DragDropTests, DragDropValidationTests, DragDropParityIntegrationTests, DialogDragDropValidatorTests, DialogDragDropTopologyTests (#2109), DropZoneHeightTests (#2382), PropertyFlushGuardTests (#2382) |
Drop validation, TreeView/FlowView parity, topology helpers, expanded-thread drop zone, reparent flush guard |
| Tree |
TreeRefreshCoordinatorTests, TreeNavigationManagerTests, TreeSelectionKeyTests, TreeViewSafeNodeBindingTests
|
Tree-state coordination |
| Stress / Edge |
DlgStressTests, DlgEdgeCaseTests, DlgCorruptionTests, DlgCrossToolTests, DlgRealWorldRoundTripTests, LazyLoadingPerformanceTests
|
Hardening |
| Integration |
Integration/PropertyPanelIntegrationTests, QuestPropertiesTests, ScriptPropertiesTests, SpeakerPropertiesTests
|
Property-panel integration |
| GUI | GUI/*Tests |
Headless UI tests |
| Services | Various *ServiceTests, Services/ModuleDirectoryResolverTests, CommandLineServiceTests, ScriptServiceCacheTests, DialogSaveServiceTests
|
Service layer |
| Window/UI |
WindowLifecycleManagerTests, SafeControlFinderTests, PanelWidthClamperTests, UISettingsServiceNodeWidthTests
|
Window/UI plumbing |
- Parley-Developer-Architecture - Code organization
- Parley-Developer-Delete-Behavior - Delete operation details
Page freshness: 2026-06-07
Getting Started
User Guide
Features
Help
- Manifest - Journal Editor
- Quartermaster - Creature/Inventory Editor
- Relique - Item Editor
- Reliquary - Placeable Editor (Alpha)
- Fence - Merchant/Store Editor
- Trebuchet - Radoub Launcher
- Marlinspike - Search and Replace
- Spell Check - Dictionary-based spell checking
- Token System - Dialog tokens and custom colors
Parley Internals
Manifest Internals
Quartermaster Internals
Relique Internals
Reliquary Internals
Fence Internals
Marlinspike (Search Engine)
Trebuchet Internals
Radoub.UI
Library
Low-Level Formats
High-Level Parsers
- JRL Format (.jrl)
- UTI Format (.uti) - Item blueprints
- UTC Format (.utc) - Creature blueprints
- UTM Format (.utm) - Store blueprints
- UTP Format (.utp) - Placeable blueprints
- UTD Format (.utd) - Door blueprints
- ARE Format (.are) - Area properties
- BIC Format (.bic) - Player characters
Original BioWare Aurora Engine file format specifications.
Core Formats
- GFF Format - Generic File Format
- KEY/BIF Format - Resource archives
- ERF Format - Encapsulated resources
- TLK Format - Talk tables
- 2DA Format - Data tables
- Localized Strings
- Common GFF Structs
Object Blueprints
- Creature Format (.utc)
- Item Format (.uti)
- Store Format (.utm)
- Door/Placeable (.utd/.utp)
- Encounter Format (.ute)
- Sound Object (.uts)
- Trigger Format (.utt)
- Waypoint Format (.utw)
Module/Area Files
- Conversation Format (.dlg)
- Journal Format (.jrl)
- Area File Format (.are/.git/.gic)
- Module Info (.ifo)
- Faction Format (.fac)
- Palette/ITP Format (.itp)
- SSF Format - Sound sets
Reference
Page freshness: 2026-05-24