Skip to content

Commit ed8ab95

Browse files
committed
tests
1 parent b6c147b commit ed8ab95

21 files changed

+354
-75
lines changed

.editorconfig

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ insert_final_newline = true
1616

1717
[*.cs]
1818
indent_size = 4
19+
tab_width = 4
20+
end_of_line = unset
1921
dotnet_sort_system_directives_first = true
22+
csharp_using_directive_placement = outside_namespace:silent
2023

2124
# Don't use this. qualifier
2225
dotnet_style_qualification_for_field = false:warning
@@ -79,6 +82,7 @@ csharp_space_between_square_brackets = false
7982
# Wrapping settings
8083
csharp_preserve_single_line_statements = false
8184
csharp_preserve_single_line_blocks = true
85+
dotnet_style_operator_placement_when_wrapping = beginning_of_line
8286

8387
# Namespace settings - C# 10+ file-scoped namespaces
8488
csharp_style_namespace_declarations = file_scoped:warning
@@ -122,10 +126,21 @@ csharp_style_prefer_utf8_string_literals = true:suggestion
122126
csharp_style_prefer_readonly_struct = true:warning
123127
csharp_style_prefer_readonly_struct_member = true:warning
124128

125-
[**/Tests/**/*.cs]
129+
[**/*Tests*/**/*.cs]
126130
dotnet_diagnostic.CA1707.severity = none
127131
dotnet_diagnostic.CA1861.severity = none
128132

133+
[DotPilot.Tests/**/*.cs]
134+
dotnet_diagnostic.CA1707.severity = none
135+
dotnet_diagnostic.CA1861.severity = none
136+
137+
[DotPilot.UITests/**/*.cs]
138+
dotnet_diagnostic.CA1707.severity = none
139+
dotnet_diagnostic.CA1861.severity = none
140+
141+
[*.xaml.cs]
142+
dotnet_diagnostic.CA1010.severity = none
143+
129144
# Use collection expressions (C# 12+)
130145
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
131146

@@ -205,8 +220,31 @@ indent_size = 2
205220
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
206221
indent_size = 2
207222

208-
[*.json]
223+
[.vsconfig]
224+
indent_size = 2
225+
end_of_line = lf
226+
227+
[*.{csproj,projitems,shproj}]
228+
indent_size = 4
229+
230+
[*.xaml]
231+
indent_size = 2
232+
233+
[*.{manifest,appxmanifest}]
234+
indent_size = 2
235+
end_of_line = lf
236+
237+
[web.config]
238+
indent_size = 2
239+
end_of_line = lf
240+
241+
[*.{json,css,webmanifest,slnf}]
242+
indent_size = 2
243+
end_of_line = lf
244+
245+
[*.{yml,yaml}]
209246
indent_size = 2
247+
end_of_line = lf
210248

211249
[*.{ps1,psm1}]
212250
indent_size = 4

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ Skill-management rules for this `.NET` solution:
127127
For this app:
128128

129129
- unit tests currently use `NUnit` through the default `VSTest` runner
130-
- UI smoke tests live in `DotPilot.UITests` and currently require local browser-driver setup before they can run
130+
- UI smoke tests live in `DotPilot.UITests` and are a mandatory part of normal verification; the harness must provision or resolve browser-driver prerequisites automatically instead of skipping when local setup is missing
131131
- `format` uses `dotnet format --verify-no-changes`
132132
- coverage uses the `coverlet.collector` integration on `DotPilot.Tests`
133133
- `LangVersion` is pinned to `latest` at the root
@@ -250,6 +250,7 @@ Local `AGENTS.md` files may tighten these values, but they must not loosen them
250250
- Repository or module coverage must not decrease without an explicit written exception. Coverage after the change must stay at least at the previous baseline or improve.
251251
- Coverage is for finding gaps, not gaming a number. Coverage numbers do not replace scenario coverage or user-flow verification.
252252
- The task is not done until the full relevant test suite is green, not only the newly added tests.
253+
- UI smoke tests are mandatory for this repository and must run in normal agent verification; missing local browser-driver setup is a harness bug to fix, not a reason to skip the suite.
253254
- For `.NET`, keep the active framework and runner model explicit so agents do not mix `TUnit`, `Microsoft.Testing.Platform`, and legacy `VSTest` assumptions.
254255
- After changing production code, run the repo-defined quality pass: format, build, analyze, focused tests, broader tests, coverage, and any configured extra gates.
255256

@@ -304,7 +305,7 @@ Ask first:
304305
- Treat `DotPilot` UI implementation as `Uno Platform` desktop XAML work, especially for Figma handoff, instead of translating designs into web stacks.
305306
- Use central package management for shared test and tooling packages.
306307
- Keep one `.NET` test framework active in the solution at a time unless a documented migration is in progress.
307-
- Validate UI changes through runnable `DotPilot.UITests` when the browser-driver path is available, instead of relying only on manual browser inspection.
308+
- Validate UI changes through runnable `DotPilot.UITests` on every relevant verification pass, instead of relying only on manual browser inspection or conditional local setup.
308309

309310
### Dislikes
310311

DotPilot.Tests/PresentationViewModelTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace DotPilot.Tests;
55
public class PresentationViewModelTests
66
{
77
[Test]
8-
public void MainViewModel_ExposesChatScreenState()
8+
public void MainViewModelExposesChatScreenState()
99
{
1010
var viewModel = new MainViewModel();
1111

@@ -20,7 +20,7 @@ public void MainViewModel_ExposesChatScreenState()
2020
}
2121

2222
[Test]
23-
public void SecondViewModel_ExposesAgentBuilderState()
23+
public void SecondViewModelExposesAgentBuilderState()
2424
{
2525
var viewModel = new SecondViewModel();
2626

DotPilot.UITests/AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ Stack: `.NET 10`, `NUnit`, `Uno.UITest`, browser-driven smoke tests
2020
- Keep this project focused on end-to-end or smoke-level verification only.
2121
- Do not add app business logic or test-only production hooks here unless they are required for stable automation.
2222
- Treat browser-driver setup and app-launch prerequisites as part of the harness, not as assumptions inside individual tests.
23+
- The harness must make `dotnet test DotPilot.UITests/DotPilot.UITests.csproj` runnable without manual driver-path export and must fail loudly instead of silently skipping coverage.
2324

2425
## Local Commands
2526

2627
- `test-ui`: `dotnet test DotPilot.UITests/DotPilot.UITests.csproj`
27-
- `test-ui-live`: `UNO_UITEST_DRIVER_PATH=/absolute/path/to/chromedriver dotnet test DotPilot.UITests/DotPilot.UITests.csproj`
28+
- `test-ui-live`: `dotnet test DotPilot.UITests/DotPilot.UITests.csproj`
2829

2930
## Applicable Skills
3031

@@ -34,5 +35,5 @@ Stack: `.NET 10`, `NUnit`, `Uno.UITest`, browser-driven smoke tests
3435

3536
## Local Risks Or Protected Areas
3637

37-
- The current harness targets a browser flow and requires `UNO_UITEST_DRIVER_PATH`; when the driver is configured it auto-starts the `net10.0-browserwasm` head on a loopback URI resolved by the harness.
38+
- The harness targets a browser flow and auto-starts the `net10.0-browserwasm` head on a loopback URI resolved by the harness; any driver discovery or bootstrap logic must stay deterministic across local and agent environments.
3839
- `Constants.cs` and `TestBase.cs` define environment assumptions for every UI test; update them carefully and only when the automation target actually changes.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
using System.Collections.ObjectModel;
2+
3+
namespace DotPilot.UITests;
4+
5+
internal static class BrowserAutomationBootstrap
6+
{
7+
private const string BrowserDriverEnvironmentVariableName = "UNO_UITEST_DRIVER_PATH";
8+
private const string BrowserBinaryEnvironmentVariableName = "UNO_UITEST_CHROME_BINARY_PATH";
9+
private const string BrowserPathEnvironmentVariableName = "UNO_UITEST_BROWSER_PATH";
10+
private const string ChromeDriverExecutableName = "chromedriver";
11+
private const string ChromeDriverExecutableNameWindows = "chromedriver.exe";
12+
private const string ChromeExecutableNameLinux = "google-chrome";
13+
private const string ChromeStableExecutableNameLinux = "google-chrome-stable";
14+
private const string ChromiumExecutableNameLinux = "chromium";
15+
private const string ChromiumBrowserExecutableNameLinux = "chromium-browser";
16+
private const string WindowsChromeRelativePath = @"Google\Chrome\Application\chrome.exe";
17+
private const string WindowsChromeLocalAppDataRelativePath = @"Google\Chrome\Application\chrome.exe";
18+
private const string MacChromeBinaryPath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
19+
private const string MacChromeForTestingBinaryPath =
20+
"/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing";
21+
private const string BrowserBinaryNotFoundMessage =
22+
"Unable to locate a Chrome browser binary for DotPilot UI smoke tests. " +
23+
"Set UNO_UITEST_CHROME_BINARY_PATH or UNO_UITEST_BROWSER_PATH explicitly.";
24+
private const string SearchedLocationsLabel = "Searched locations:";
25+
private static readonly ReadOnlyCollection<string> DefaultBrowserBinaryCandidates =
26+
CreateDefaultBrowserBinaryCandidates();
27+
28+
public static BrowserAutomationSettings Resolve()
29+
{
30+
return Resolve(CreateEnvironmentSnapshot(), DefaultBrowserBinaryCandidates);
31+
}
32+
33+
internal static BrowserAutomationSettings Resolve(
34+
IReadOnlyDictionary<string, string?> environment,
35+
IReadOnlyList<string> browserBinaryCandidates)
36+
{
37+
var driverPath = NormalizeBrowserDriverPath(environment);
38+
var browserBinaryPath = ResolveBrowserBinaryPath(environment, browserBinaryCandidates);
39+
SetEnvironmentVariableIfMissing(BrowserBinaryEnvironmentVariableName, browserBinaryPath, environment);
40+
SetEnvironmentVariableIfMissing(BrowserPathEnvironmentVariableName, browserBinaryPath, environment);
41+
42+
return new BrowserAutomationSettings(driverPath, browserBinaryPath);
43+
}
44+
45+
private static string? NormalizeBrowserDriverPath(IReadOnlyDictionary<string, string?> environment)
46+
{
47+
if (!environment.TryGetValue(BrowserDriverEnvironmentVariableName, out var configuredPath) ||
48+
string.IsNullOrWhiteSpace(configuredPath))
49+
{
50+
return null;
51+
}
52+
53+
if (File.Exists(configuredPath))
54+
{
55+
var directory = Path.GetDirectoryName(configuredPath);
56+
if (!string.IsNullOrWhiteSpace(directory))
57+
{
58+
Environment.SetEnvironmentVariable(BrowserDriverEnvironmentVariableName, directory);
59+
return directory;
60+
}
61+
}
62+
63+
if (Directory.Exists(configuredPath))
64+
{
65+
var driverPath = Path.Combine(configuredPath, GetChromeDriverExecutableFileName());
66+
if (File.Exists(driverPath))
67+
{
68+
return configuredPath;
69+
}
70+
}
71+
72+
return null;
73+
}
74+
75+
private static string ResolveBrowserBinaryPath(
76+
IReadOnlyDictionary<string, string?> environment,
77+
IReadOnlyList<string> browserBinaryCandidates)
78+
{
79+
foreach (var environmentVariableName in GetBrowserBinaryEnvironmentVariableNames())
80+
{
81+
if (environment.TryGetValue(environmentVariableName, out var configuredPath) &&
82+
!string.IsNullOrWhiteSpace(configuredPath) &&
83+
File.Exists(configuredPath))
84+
{
85+
return configuredPath;
86+
}
87+
}
88+
89+
foreach (var candidatePath in browserBinaryCandidates)
90+
{
91+
if (File.Exists(candidatePath))
92+
{
93+
return candidatePath;
94+
}
95+
}
96+
97+
var searchedLocations = browserBinaryCandidates.Count == 0
98+
? " none"
99+
: $"{Environment.NewLine}- {string.Join($"{Environment.NewLine}- ", browserBinaryCandidates)}";
100+
101+
throw new InvalidOperationException($"{BrowserBinaryNotFoundMessage}{Environment.NewLine}{SearchedLocationsLabel}{searchedLocations}");
102+
}
103+
104+
private static Dictionary<string, string?> CreateEnvironmentSnapshot()
105+
{
106+
return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
107+
{
108+
[BrowserDriverEnvironmentVariableName] = Environment.GetEnvironmentVariable(BrowserDriverEnvironmentVariableName),
109+
[BrowserBinaryEnvironmentVariableName] = Environment.GetEnvironmentVariable(BrowserBinaryEnvironmentVariableName),
110+
[BrowserPathEnvironmentVariableName] = Environment.GetEnvironmentVariable(BrowserPathEnvironmentVariableName),
111+
};
112+
}
113+
114+
private static void SetEnvironmentVariableIfMissing(
115+
string environmentVariableName,
116+
string value,
117+
IReadOnlyDictionary<string, string?> environment)
118+
{
119+
if (environment.TryGetValue(environmentVariableName, out var configuredValue) &&
120+
!string.IsNullOrWhiteSpace(configuredValue))
121+
{
122+
return;
123+
}
124+
125+
Environment.SetEnvironmentVariable(environmentVariableName, value);
126+
}
127+
128+
private static ReadOnlyCollection<string> CreateDefaultBrowserBinaryCandidates()
129+
{
130+
var candidates = new List<string>();
131+
132+
if (OperatingSystem.IsMacOS())
133+
{
134+
candidates.Add(MacChromeBinaryPath);
135+
candidates.Add(MacChromeForTestingBinaryPath);
136+
return candidates.AsReadOnly();
137+
}
138+
139+
if (OperatingSystem.IsLinux())
140+
{
141+
candidates.Add(Path.Combine(Path.DirectorySeparatorChar.ToString(), "usr", "bin", ChromeExecutableNameLinux));
142+
candidates.Add(Path.Combine(Path.DirectorySeparatorChar.ToString(), "usr", "bin", ChromeStableExecutableNameLinux));
143+
candidates.Add(Path.Combine(Path.DirectorySeparatorChar.ToString(), "usr", "bin", ChromiumExecutableNameLinux));
144+
candidates.Add(Path.Combine(Path.DirectorySeparatorChar.ToString(), "usr", "bin", ChromiumBrowserExecutableNameLinux));
145+
return candidates.AsReadOnly();
146+
}
147+
148+
if (OperatingSystem.IsWindows())
149+
{
150+
AddWindowsBrowserCandidate(candidates, Environment.SpecialFolder.ProgramFiles, WindowsChromeRelativePath);
151+
AddWindowsBrowserCandidate(candidates, Environment.SpecialFolder.ProgramFilesX86, WindowsChromeRelativePath);
152+
AddWindowsBrowserCandidate(candidates, Environment.SpecialFolder.LocalApplicationData, WindowsChromeLocalAppDataRelativePath);
153+
}
154+
155+
return candidates.AsReadOnly();
156+
}
157+
158+
private static void AddWindowsBrowserCandidate(
159+
List<string> candidates,
160+
Environment.SpecialFolder specialFolder,
161+
string relativePath)
162+
{
163+
var rootPath = Environment.GetFolderPath(specialFolder);
164+
if (string.IsNullOrWhiteSpace(rootPath))
165+
{
166+
return;
167+
}
168+
169+
candidates.Add(Path.Combine(rootPath, relativePath));
170+
}
171+
172+
private static string GetChromeDriverExecutableFileName()
173+
{
174+
return OperatingSystem.IsWindows()
175+
? ChromeDriverExecutableNameWindows
176+
: ChromeDriverExecutableName;
177+
}
178+
179+
private static IEnumerable<string> GetBrowserBinaryEnvironmentVariableNames()
180+
{
181+
yield return BrowserBinaryEnvironmentVariableName;
182+
yield return BrowserPathEnvironmentVariableName;
183+
}
184+
}
185+
186+
internal sealed record BrowserAutomationSettings(string? DriverPath, string BrowserBinaryPath);

0 commit comments

Comments
 (0)