-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprogress.txt
More file actions
296 lines (276 loc) · 28.2 KB
/
progress.txt
File metadata and controls
296 lines (276 loc) · 28.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
## Codebase Patterns
- xUnit v3 (xunit.v3 1.1.0) requires explicit `using Xunit;` — not included in ImplicitUsings
- Test files go in mirrored folder structure: tests/TurdTracker.Tests/Services/ for service tests, tests/TurdTracker.Tests/Components/ for component tests
- bUnit component tests: use `BunitContext` with `JSRuntimeMode.Loose` and `AddMudServices()` for MudBlazor components
- Components using MudTextField with OnKeyDown (KeyInterceptorService) require `IAsyncDisposable` + `DisposeAsync()` instead of `IDisposable` in test class
- Registering services in bUnit: `_ctx.Services.AddSingleton<IService>(fake)` requires `using Microsoft.Extensions.DependencyInjection;`
- bUnit `EventCallback.Factory.Create<T>(this, handler)` requires `using Microsoft.AspNetCore.Components;`
- Build command: `DOTNET_CLI_USE_MSBUILD_SERVER=0 DOTNET_HOST_PATH=$(which dotnet) dotnet build` (required on macOS ARM64 to avoid task host error)
- `wasm-tools` workload must be installed (`dotnet workload install wasm-tools`) — without it, ComputeWasmBuildAssets fails with MSB4216
- Build with sandbox disabled for NuGet restore (network access required)
- Project source lives in `src/TurdTracker/`
- MudBlazor v9.2.0 configured with providers in MainLayout.razor
- Blazored.LocalStorage v4.5.0 (must specify version explicitly; latest search fails on .NET 10)
- Folder structure: Pages/, Components/, Models/, Services/ under src/TurdTracker/
- MudBlazor CSS/JS referenced in wwwroot/index.html
- Build from repo root: `DOTNET_CLI_USE_MSBUILD_SERVER=0 dotnet build src/TurdTracker/TurdTracker.csproj`
- DiaryService uses LocalStorage key "diary-entries"
- DI registration pattern: `builder.Services.AddScoped<IService, Service>()` in Program.cs
- `_Imports.razor` has global `@using TurdTracker.Services` — no per-page using needed
- Use `InvokeAsync(StateHasChanged)` when handling events from services (not on Blazor sync context)
- MergeEngine is static — call `MergeEngine.Merge(local, remote)` directly
- SyncService subscribes to DiaryService.OnDataChanged with 2-second debounce
- SyncService handles 412 (re-download + re-merge + retry up to 3x) and 401 (silent re-auth) automatically
- OnDataMerged event: SyncService fires this after ReplaceAllAsync when localChanged is true
- JS interop: `IJSRuntime.InvokeAsync("googleAuth.method", args)` — no `window.` prefix
- Google Drive API: files at `https://www.googleapis.com/drive/v3/files`, upload at `https://www.googleapis.com/upload/drive/v3/files`
- Hand-rolled fakes only — no Moq, NSubstitute, or any mocking framework
- Test project at tests/TurdTracker.Tests/ using xUnit + bUnit + FluentAssertions
- Fakes go in tests/TurdTracker.Tests/Fakes/ folder
- Use bUnit's built-in JSInterop for JS call verification (not hand-rolled IJSRuntime fake)
- FakeLocalStorageService backed by Dictionary<string, object> — uses ValueTask returns, nullable T? on GetItemAsync
- FakeDiaryService.SeedEntries() for test setup without triggering events or recording method calls
- FakeGoogleDriveService.CreateHttpException(statusCode) helper for creating typed HTTP exceptions
- FakeSyncService has extra OnDataMerged event (not yet on ISyncService interface)
- Test file naming: {ClassUnderTest}Tests.cs in mirrored folder structure
- Run tests: `dotnet test tests/TurdTracker.Tests/TurdTracker.Tests.csproj`
- bUnit JSInterop: use `JSRuntimeMode.Loose` for services that make many JS calls; verify with `_ctx.JSInterop.Invocations["identifier"]`
- bUnit v2.0.33-preview: use `BunitContext` (not `TestContext` which is ambiguous with xUnit v3's `Xunit.TestContext`)
- bUnit JSInterop `Setup<T>(identifier).SetResult(value)` for typed returns; void calls handled automatically in Loose mode
- GoogleDriveService: construct with `new GoogleDriveService(new HttpClient(fakeHandler), fakeAuthService)` — no DI needed
- DiaryEntry.Id is `Guid` (not string) — use `Guid.NewGuid()` directly
- FluentAssertions v8: method is `BeGreaterThanOrEqualTo` (not `BeGreaterOrEqualTo`)
- SyncService.Dispose() doesn't guard against double-dispose — use separate instances in Dispose tests to avoid class IDisposable conflicts
- For retry/error testing, use inline fakes with Queue<Exception> to control per-call behavior (e.g., ConfigurableDriveService)
- ISyncService now has `event Action? OnDataMerged` — pages subscribe in OnInitializedAsync, unsubscribe in Dispose
- bUnit NavigationManager: `_ctx.Services.GetRequiredService<NavigationManager>()` — no FakeNavigationManager in bUnit v2
- Page tests go in tests/TurdTracker.Tests/Pages/ — use IAsyncDisposable for MudBlazor components
- bUnit renders `<b>` with Blazor scope attributes (e.g. `<b b-xxx>`) — use CSS selector `.calendar-cell b` instead of `InnerHtml.Contains("<b>")`
- MudIconButton renders as `button.mud-icon-button` — select by CSS class, not by icon name in outerHtml
- Empty calendar cells have no innerHTML — use `string.IsNullOrWhiteSpace(c.InnerHtml)` to identify offset cells
- MudBlazor BarChart always renders SVG even when all data values are zero — `_frequencyData.Length == 0` never triggers because BuildFrequencyChart creates array with `days` elements; use `.All(d => d == 0)` check if you want to show empty state
- MudBlazor BarChart: select bars via `.mud-chart-bar` CSS class inside `svg.mud-chart-bar`; bar count equals data array length
- 30-day frequency chart labels only show every 5th day (`i % 5 == 0`) — don't rely on date labels for assertions; use bar count instead
- Stats page: MudChipSet `SelectedValueChanged` triggers `OnRangeChanged` — click `.mud-chip` elements to switch range in tests
- MudDatePicker/MudTimePicker require `MudPopoverProvider` in the render tree — render `_ctx.Render<MudPopoverProvider>()` before rendering the component under test
- MudIconButton: select Edit by `button.mud-icon-button.mud-primary-text`, Delete by `button.mud-icon-button.mud-error-text` — SVG icons have no text content
- FakeDialogService: `IDialogService.DialogInstanceAddedAsync` is `event Func<IDialogReference, Task>?`, `OnDialogCloseRequested` is `event Action<IDialogReference, DialogResult?>?` — not EventHandler types
- IDialogService.ShowAsync returns `Task<IDialogReference>` (not bare IDialogReference)
- bUnit JSInterop void call verification: use `SetupVoid("identifier")` + `VerifyInvoke("identifier")` — Loose mode handles the call but doesn't track it for verification without explicit Setup
- Reflection to set private fields for testing: `typeof(Component).GetField("_field", BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(instance, value)` — useful for MudDatePicker bound values
- MudBlazor Color.Default renders buttons WITHOUT a color-text CSS class — select by excluding known classes (mud-success-text, mud-error-text, etc.)
- MudTooltip v9: trigger `PointerEnter()` on `.mud-tooltip-root` to show tooltip popover content (not MouseOver)
- LayoutComponentBase tests: `_ctx.Render<Layout>(p => p.Add(x => x.Body, builder => builder.AddContent(0, "text")))`
- Theme toggle in appbar: filter buttons by `Closest(".mud-tooltip-root") == null` to distinguish from sync icon button
---
## 2026-03-25 21:28 - US-001
- Created xUnit test project at tests/TurdTracker.Tests/ with bUnit, FluentAssertions, Blazored.LocalStorage, MudBlazor
- Created GitHub Actions workflow at .github/workflows/test.yml triggering on PRs to main
- Project references src/TurdTracker/TurdTracker.csproj, builds and runs with zero tests
- **Learnings for future iterations:**
- bUnit v2.0.33-preview is the version compatible with .NET 10
- xunit.v3 v1.1.0 + xunit.runner.visualstudio v3.1.0 for xUnit v3 on .NET 10
- FluentAssertions v8.3.0 works with .NET 10
- Microsoft.NET.Test.Sdk v17.13.0 needed for test discovery
---
## 2026-03-25 22:00 - US-002
- Created 7 hand-rolled fakes in tests/TurdTracker.Tests/Fakes/:
- FakeLocalStorageService: Dictionary-backed ILocalStorageService with all methods
- FakeDiaryService: In-memory List<DiaryEntry>, tracks MethodCalls, has SeedEntries() helper
- FakeGoogleAuthService: Configurable return values for all methods, tracks MethodCalls
- FakeGoogleDriveService: Configurable results/exceptions, records upload details, has CreateHttpException() helper
- FakeSyncService: Settable SyncStatus/LastError/LastSyncedUtc, invocable OnSyncStatusChanged/OnDataMerged events
- FakeThemeService: In-memory dark mode state (defaults to true)
- FakeHttpMessageHandler: URL-pattern and predicate-based response matching, tracks SentRequests
- Files changed: 7 new files in tests/TurdTracker.Tests/Fakes/
- **Learnings for future iterations:**
- Blazored.LocalStorage 4.5.0 uses ValueTask returns and nullable T? on GetItemAsync
- ILocalStorageService has ChangingEventArgs/ChangedEventArgs events — suppress CS0067 in fake
- ISyncService doesn't have OnDataMerged yet — added as extra public event on FakeSyncService for future test stories
- FakeDiaryService.SeedEntries() bypasses events/call-tracking for clean test setup
---
## 2026-03-25 22:35 - US-003
- Created 9 MergeEngine unit tests in tests/TurdTracker.Tests/Services/MergeEngineTests.cs
- Covers: empty merge, local-only, remote-only, local-wins, remote-wins, tie-break, tombstone purge (old/young), mixed scenario
- Files changed: 1 new file (MergeEngineTests.cs)
- **Learnings for future iterations:**
- xUnit v3 requires explicit `using Xunit;` — ImplicitUsings does not include it
- MergeEngine tie-break: equal LastModified → local wins but neither changed flag is set (code uses strict > for remoteChanged)
- MergeEngine is static — test directly with `MergeEngine.Merge(local, remote)`, no DI needed
---
## 2026-03-25 23:10 - US-004
- Created 11 DiaryService unit tests in tests/TurdTracker.Tests/Services/DiaryServiceTests.cs
- Covers: AddAsync (store + LastModified + event), GetAllAsync (filters deleted), GetAllIncludingDeletedAsync, GetByIdAsync (found + not found), GetByDateAsync, UpdateAsync (update + event), UpdateAsync (non-existent ID no event), DeleteAsync (soft-delete + LastModified + event), ReplaceAllAsync (no event), BackfillLastModifiedIfNeeded (migration)
- Files changed: 1 new file (DiaryServiceTests.cs)
- **Learnings for future iterations:**
- DiaryService uses real `new DiaryService(fakeLocalStorage)` — no DI container needed for unit tests
- AddAsync sets IsDeleted entries into storage as-is — the filtering happens in GetAllAsync
- BackfillLastModifiedIfNeeded only runs once per DiaryService instance (_hasMigrated flag) — seed data directly into FakeLocalStorageService to test migration
- DiaryService storage key is "diary-entries" — use this to seed test data directly into FakeLocalStorageService
---
## 2026-03-25 - US-005
- Created 3 ThemeService unit tests in tests/TurdTracker.Tests/Services/ThemeServiceTests.cs
- Covers: GetIsDarkModeAsync default (true when no value), GetIsDarkModeAsync returns stored value, SetIsDarkModeAsync writes to localStorage
- Files changed: 1 new file (ThemeServiceTests.cs)
- **Learnings for future iterations:**
- ThemeService storage key is "theme-dark-mode"
- ThemeService defaults to dark mode (true) when no localStorage value exists
- Simple services like ThemeService can be tested with just FakeLocalStorageService — no DI container needed
---
## 2026-03-25 - US-006
- Created 7 GoogleAuthService unit tests in tests/TurdTracker.Tests/Services/GoogleAuthServiceTests.cs
- Covers: InitializeAsync idempotency, SignInAsync (ensure init + JS call), SignOutAsync, IsSignedInAsync (ensure init + JS call), GetAccessTokenAsync, TrySilentSignInAsync (true when token, false when null)
- Files changed: 1 new file (GoogleAuthServiceTests.cs)
- **Learnings for future iterations:**
- bUnit `TestContext` is ambiguous with xUnit v3's `Xunit.TestContext` — use `BunitContext` instead
- bUnit JSInterop in strict mode requires explicit `SetupVoid` with argument matchers for each call; `JSRuntimeMode.Loose` is simpler for service-level tests
- GoogleAuthService uses double-check locking pattern with SemaphoreSlim for initialization — tests verify idempotency
- `Setup<string?>("identifier").SetResult((string?)null)` to test null return values
---
## 2026-03-25 - US-007
- Created 6 GoogleDriveService unit tests in tests/TurdTracker.Tests/Services/GoogleDriveServiceTests.cs
- Covers: FindSyncFileAsync (file exists → returns id/etag, no file → nulls), DownloadSyncFileAsync (deserializes SyncEnvelope), UploadSyncFileAsync (create new file when fileId null, update existing with If-Match etag, 412 conflict throws HttpRequestException)
- Files changed: 1 new file (GoogleDriveServiceTests.cs)
- **Learnings for future iterations:**
- GoogleDriveService takes HttpClient + IGoogleAuthService via constructor — use FakeHttpMessageHandler with HttpClient for testing
- FakeHttpMessageHandler URL pattern matching is substring-based — use distinctive URL fragments like "alt=media", "uploadType=multipart", "uploadType=media"
- DiaryEntry.Id is `Guid` not `string` — use `Guid.NewGuid()` directly
- EnsureSuccessStatusCode() throws HttpRequestException on non-2xx — no need to simulate specific exception types for 412/401 tests
- SyncService concurrency guard: if SyncStatus == Syncing, SyncAsync returns immediately — use a SlowFake with TaskCompletionSource to test this
- SyncService has no OnDataMerged event — PRD references it but it was never merged from archived feature branch
- For testing InitializeAsync with silent sign-in, need a custom fake that switches IsSignedIn to true after TrySilentSignInAsync (standard FakeGoogleAuthService can't model this state change)
---
## 2026-03-25 - US-008
- Created 9 SyncService unit tests in tests/TurdTracker.Tests/Services/SyncServiceTests.cs
- Covers: InitializeAsync (signed-in → sync, silent sign-in → sync, no session → NotSignedIn), SyncAsync (not signed in, happy path status transitions, concurrent call guard), merge results (LocalChanged → ReplaceAllAsync, RemoteChanged → UploadSyncFileAsync, OnDataChanged resubscription)
- Helper fakes: FakeGoogleAuthServiceWithSilentSignInSwitch (models sign-in state change), SlowFakeGoogleDriveService (TaskCompletionSource for concurrency testing)
- Files changed: 1 new file (SyncServiceTests.cs)
- **Learnings for future iterations:**
- SyncService concurrency: SyncAsync returns immediately if SyncStatus == Syncing — use SlowFake with TaskCompletionSource to block first call while attempting second
- SyncService InitializeAsync flow: checks IsSignedIn → HasPreviousSession → TrySilentSignIn, each branch needs its own test setup
- OnDataMerged is referenced in PRD but not implemented in SyncService — archived feature branch was never merged
- FakeGoogleAuthService can't model state transitions (e.g. IsSignedIn changing after TrySilentSignIn) — need custom inline fakes for these scenarios
---
## 2026-03-25 - US-009
- Created 10 SyncService error/retry/debounce tests in tests/TurdTracker.Tests/Services/SyncServiceTests.cs
- Covers: 412 retry (success + exhausted), 401 re-auth retry, network error revert, 500 error, general exception, LastError cleared, debounce cancellation, OnDataChanged re-subscription on ReplaceAll exception, Dispose cancels debounce
- Helper fakes: ConfigurableDriveService (queue-based upload exceptions), ThrowingDriveService (always throws), ThrowingOnReplaceDiaryService (throws on ReplaceAllAsync with toggle)
- Files changed: 1 modified file (SyncServiceTests.cs — added ~339 lines)
- **Learnings for future iterations:**
- FluentAssertions v8: use `BeGreaterThanOrEqualTo` not `BeGreaterOrEqualTo`
- SyncService.Dispose() does not null out _debounceCts after disposing — calling Dispose twice causes ObjectDisposedException; use separate SyncService instances in Dispose tests to avoid class-level IDisposable double-dispose
- Debounce tests require ~3.5s wait (2s delay + margin) — acceptable for unit tests but slows suite
- ConfigurableDriveService with Queue<Exception> for upload exceptions is the most flexible pattern for retry testing — dequeues one exception per call, succeeds when empty
- HttpRequestException with null StatusCode (network error) is distinct from one with a status code — SyncService handles them differently
---
## 2026-03-25 - US-010
- Created 4 BristolScaleSelector bUnit component tests in tests/TurdTracker.Tests/Components/BristolScaleSelectorTests.cs
- Covers: renders all 7 Bristol type cards with names, selected card has bristol-card-selected class, clicking card invokes SelectedTypeChanged with correct value, no card selected when SelectedType=0
- Files changed: 1 new file (Components/BristolScaleSelectorTests.cs)
- **Learnings for future iterations:**
- bUnit component tests need `BunitContext` with `JSRuntimeMode.Loose` and `AddMudServices()` for MudBlazor components
- `EventCallback.Factory.Create<T>(receiver, handler)` requires `using Microsoft.AspNetCore.Components;` — not included by default
- MudBlazor renders CSS classes on elements — use `.bristol-card` CSS selector to find cards, check `.ClassList` for selected state
- Component tests go in tests/TurdTracker.Tests/Components/ mirroring the source structure
---
## 2026-03-26 - US-011
- Created 5 TagInput bUnit component tests in tests/TurdTracker.Tests/Components/TagInputTests.cs
- Covers: add tag via Enter key (TagsChanged callback), duplicate tag case-insensitive rejection, remove tag (TagsChanged callback), text field cleared after add, recent tags loaded from DiaryService (top 10, deduped, excluding current)
- Fixed compilation: added `using Microsoft.Extensions.DependencyInjection;` for `AddSingleton` on `BunitServiceProvider`
- Fixed disposal: MudTextField's OnKeyDown registers KeyInterceptorService (IAsyncDisposable) — switched from `IDisposable` to `IAsyncDisposable` with `DisposeAsync()`
- Files changed: 1 new file (Components/TagInputTests.cs)
- **Learnings for future iterations:**
- `BunitServiceProvider` doesn't have `AddSingleton` directly — need `using Microsoft.Extensions.DependencyInjection;` for extension methods
- Components with MudTextField + OnKeyDown register MudBlazor's `KeyInterceptorService` which only implements `IAsyncDisposable` — test class must use `IAsyncDisposable` + `DisposeAsync()` instead of `IDisposable`
- bUnit `input.Change("value")` + `input.KeyDown(new KeyboardEventArgs { Key = "Enter" })` to simulate typing and pressing Enter
- MudChip close buttons found via `.mud-chip button` selector
---
## 2026-03-26 - US-012
- Created 8 Home page bUnit component tests in tests/TurdTracker.Tests/Pages/HomeTests.cs
- Covers: empty entries message + Log Entry button, entry cards (Bristol type, timestamp, truncated notes at 80 chars, tags), clicking card navigates to /entry/{Id}, sync banner shown when not signed in and not dismissed, banner hidden when signed in, banner hidden when dismissed, OnDataMerged refreshes entries, Dispose unsubscribes OnDataMerged
- Added `event Action? OnDataMerged` to ISyncService interface and SyncService implementation (fires after ReplaceAllAsync when localChanged)
- Updated Home.razor to subscribe to OnDataMerged (refresh entries) and implement IDisposable (unsubscribe)
- Files changed: ISyncService.cs (added OnDataMerged), SyncService.cs (added OnDataMerged event + fire), Home.razor (subscribe/dispose), HomeTests.cs (new)
- **Learnings for future iterations:**
- ISyncService now has `event Action? OnDataMerged` — all pages that refresh on sync should subscribe to this
- bUnit NavigationManager: use `_ctx.Services.GetRequiredService<NavigationManager>()` (from Microsoft.AspNetCore.Components) — bUnit v2 doesn't expose FakeNavigationManager directly
- Home.razor OnAfterRenderAsync runs after first render — use `cut.WaitForState()` in tests to wait for async state changes
- Page test files go in tests/TurdTracker.Tests/Pages/ mirroring the source structure
- `async void` event handlers with `InvokeAsync(StateHasChanged)` for cross-thread UI updates from service events
---
## 2026-03-26 - US-013
- Created 8 Calendar page bUnit component tests in tests/TurdTracker.Tests/Pages/CalendarTests.cs
- Covers: correct number of days for current month, Monday-start day-of-week offset, entry count badges on dates with entries, clicking date selects and shows entries, previous/next month navigation, empty selected date shows "No entries for this day", OnDataMerged refreshes entries, Dispose unsubscribes OnDataMerged
- Updated Calendar.razor to inject ISyncService, subscribe to OnDataMerged, implement IDisposable, and extracted RebuildEntriesByDate() helper
- Files changed: Calendar.razor (added ISyncService injection, OnDataMerged subscription, IDisposable), CalendarTests.cs (new)
- **Learnings for future iterations:**
- bUnit renders `<b>` tags with Blazor scope attributes (e.g. `<b b-5a6vurbgxm>`) — never match `<b>` as literal string in InnerHtml; use CSS selectors like `.calendar-cell b` instead
- MudIconButton renders as `button.mud-icon-button` — select by CSS class, don't look for icon name strings in outerHtml
- Empty calendar offset cells have no innerHTML — use `string.IsNullOrWhiteSpace(c.InnerHtml)` to distinguish from day cells
- `QuerySelectorAll("b")` on individual elements works for finding nested elements within a bUnit-found node
---
## 2026-03-26 - US-014
- Created 7 Stats page bUnit component tests in tests/TurdTracker.Tests/Pages/StatsTests.cs
- Covers: frequency chart daily counts for 7-day range, Bristol distribution tallies types 1-7, time of day chart buckets by hour, time range chip selection rebuilds frequency chart (7→30 bars), empty data renders without errors, OnDataMerged rebuilds charts, Dispose unsubscribes OnDataMerged
- Updated Stats.razor to inject ISyncService, subscribe to OnDataMerged, implement IDisposable with chart rebuild on sync
- Files changed: Stats.razor (added ISyncService injection, OnDataMerged subscription, IDisposable), StatsTests.cs (new)
- **Learnings for future iterations:**
- MudBlazor BarChart always renders SVG even with all-zero data — `_frequencyData.Length == 0` never triggers; use `.All(d => d == 0)` for empty state detection
- 30-day frequency chart labels show every 5th day only — don't assert on specific date labels; assert bar count instead (7 bars vs 30 bars)
- MudBlazor BarChart bars selectable via `.mud-chart-bar` class inside `svg.mud-chart-bar` — bar count equals data array length
- Stats page has 3 independent chart sections — each has its own empty-state check and SVG rendering
---
## 2026-03-26 - US-015
- Created 4 Log page bUnit component tests in tests/TurdTracker.Tests/Pages/LogTests.cs
- Covers: save with BristolType=0 shows validation error (no AddAsync call), save with valid data calls AddAsync with correct entry and navigates to /, entry timestamp combines date and time, cancel navigates to /
- Files changed: 1 new file (Pages/LogTests.cs)
- **Learnings for future iterations:**
- MudDatePicker/MudTimePicker require `MudPopoverProvider` in the render tree — render `_ctx.Render<MudPopoverProvider>()` before the component under test
- Log page uses default `DateTime.Today` and `DateTime.Now.TimeOfDay` — test timestamp by asserting date is today and time is non-zero
- Bristol card selection: click `.bristol-card` elements by index (0-based, type = index + 1)
- bUnit NavigationManager: assert `nav.Uri.Should().EndWith("/")` for home navigation (NavigateTo("") resolves to base URI with trailing slash)
---
## 2026-03-26 - US-016
- Created FakeDialogService in tests/TurdTracker.Tests/Fakes/ implementing IDialogService with configurable MessageBoxResult and MethodCalls tracking
- Created 6 EntryDetail page bUnit component tests in tests/TurdTracker.Tests/Pages/EntryDetailTests.cs
- Covers: loads/displays entry by ID (Bristol type, timestamp, notes, tags), entry not found error, edit button toggles edit form with pre-populated fields, save in edit mode calls UpdateAsync and exits edit mode, cancel in edit mode exits without saving, delete shows confirmation and calls DeleteAsync then navigates home
- Files changed: FakeDialogService.cs (new), EntryDetailTests.cs (new)
- **Learnings for future iterations:**
- IDialogService has `event Func<IDialogReference, Task>? DialogInstanceAddedAsync` and `event Action<IDialogReference, DialogResult?>? OnDialogCloseRequested` — not standard EventHandler types
- IDialogService.ShowAsync methods return `Task<IDialogReference>` not bare `IDialogReference`
- MudIconButton rendered as SVG icons — no text content; select Edit by `button.mud-icon-button.mud-primary-text`, Delete by `button.mud-icon-button.mud-error-text`
- FakeDialogService with configurable `MessageBoxResult` property is the cleanest way to test dialog confirmation flows
---
## 2026-03-26 - US-017
- Created 6 Settings page bUnit component tests in tests/TurdTracker.Tests/Pages/SettingsTests.cs
- Covers: sign-in calls SignInAsync and on success sets signed-in state + SyncAsync, sign-out calls SignOutAsync then SyncAsync, Sync Now calls SyncAsync, buttons disabled when SyncStatus==Syncing, sync status text/icon reflects current status (with OnSyncStatusChanged event updates), LastSyncedUtc displayed when available
- Files changed: 1 new file (Pages/SettingsTests.cs)
- **Learnings for future iterations:**
- Settings page uses OnAfterRenderAsync (firstRender) to load state — bUnit triggers this automatically on Render
- Sign-in button is `button.mud-button-filled`, sign-out/sync-now are `button.mud-button-outlined` — distinguish by TextContent
- FakeSyncService.RaiseOnSyncStatusChanged() triggers InvokeAsync(StateHasChanged) in Settings page — use cut.WaitForState() to assert re-render
- Settings page subscribes to OnSyncStatusChanged in OnAfterRenderAsync and unsubscribes in Dispose
---
## 2026-03-26 - US-018
- Created 6 Export page bUnit component tests in tests/TurdTracker.Tests/Pages/ExportTests.cs
- Updated Export.razor to inject ISyncService, subscribe to OnDataMerged, implement IDisposable with entry refresh on sync
- Covers: all entries shown when no date filter, start date filter excludes before, end date filter excludes after, both filters combined, export button calls window.print via JS interop, OnDataMerged refreshes entries
- Files changed: Export.razor (added ISyncService injection, OnDataMerged subscription, IDisposable), ExportTests.cs (new)
- **Learnings for future iterations:**
- MudDatePicker private field values can be set via reflection for testing date filtering (typeof(Export).GetField("_startDate", BindingFlags.NonPublic | BindingFlags.Instance))
- PrintExport has Task.Delay(100) — async tests need await Task.Delay(200) before verifying JS interop calls
- bUnit JSInterop: use SetupVoid("window.print") + VerifyInvoke("window.print") for void JS calls — Loose mode auto-handles but doesn't track for verification
- Export page filter is applied in PrintExport() and OnInitializedAsync() — clicking export button re-applies current filter settings
---
## 2026-03-26 - US-019
- Created 11 MainLayout bUnit component tests in tests/TurdTracker.Tests/Layout/MainLayoutTests.cs
- Added OnDataMerged subscription to MainLayout.razor — fires "Sync complete" snackbar when status is Synced or Idle
- Covers: sync icon CloudDone/Success when Synced, CloudSync/Inherit+spin when Syncing, CloudOff/Error when Error, CloudOff/Default when NotSignedIn, clicking error sync icon shows snackbar with LastError, OnDataMerged shows "Sync complete" snackbar (Synced + Idle), no snackbar when Syncing, theme toggle dark/light mode, tooltip shows status + LastSyncedUtc, tooltip shows "Not synced yet" when no last sync
- Files changed: MainLayout.razor (added OnDataMerged subscription/dispose + OnDataMerged handler with snackbar), MainLayoutTests.cs (new)
- **Learnings for future iterations:**
- MudBlazor Color.Default renders icon buttons WITHOUT a color-text CSS class — select by excluding known color classes (mud-success-text, mud-error-text, mud-inherit-text, etc.)
- MudTooltip v9 uses pointer events — trigger `PointerEnter()` on `.mud-tooltip-root` to show tooltip content in popover (not `MouseOver`)
- Theme toggle button: distinguish from sync icon by filtering out buttons inside `.mud-tooltip-root` using `Closest(".mud-tooltip-root") == null`
- LayoutComponentBase tests: render with `parameters.Add(p => p.Body, builder => builder.AddContent(0, "content"))` for body slot
- MudSnackbar: `Snackbar.Add(message, severity)` renders message content in `MudSnackbarProvider` — assertable via `cut.Markup.Contains(message)`
---