From ecb329e57865035987f1f4237a25884556b361b5 Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 25 Sep 2026 08:18:25 -0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20drawers=20close=20on=20any=20click?= =?UTF-8?q?=20outside=20the=20rail=20=E2=80=94=20TEST=20was=20the=20missin?= =?UTF-8?q?g=20third?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creator: 'when the test slide-out is active, then any click off the div should close the div — this behaviour applies to all tabs, not just TEST.' MainWindow.Window_PreviewMouseLeftButtonDown already closed the Stream Settings and YPP drawers on any click outside TextPullOutHost, but its close list skipped TestSession — so the TEST drawer never dismissed on an outside click. Fix: run TestSession.CloseDrawerCommand in the same outside-click branch. All three drawers share the rail host, so the containment check is unchanged. Good Dog: ONE integration test — the existing TestStream window section now raises a window-root PreviewMouseLeftButtonDown (source = window => outside the rail; deterministic, no OS mouse) and asserts the TEST and Stream Settings drawers both close. Gate: clean build 0 warnings, full vstest 318/318. --- Controls/PreviewPane.xaml | 2 +- HANDOFF.md | 56 ++++++++++----------- MainWindow.xaml.cs | 2 + TASKS.md | 1 + TASKS/task-46-drawer-click-outside-close.md | 37 ++++++++++++++ ai.md | 5 +- ytLive.Tests/TestStreamTests.cs | 20 ++++++++ 7 files changed, 91 insertions(+), 32 deletions(-) create mode 100644 TASKS/task-46-drawer-click-outside-close.md diff --git a/Controls/PreviewPane.xaml b/Controls/PreviewPane.xaml index 8097495..f87b90b 100644 --- a/Controls/PreviewPane.xaml +++ b/Controls/PreviewPane.xaml @@ -418,7 +418,7 @@ edge; each toggles its own drawer (the same animated-width pattern). Only one drawer is open at a time — IsDrawerOpen exclusivity is VM-enforced (opening one collapses the other). Any click outside - both collapses whichever is open (MainWindow code-behind). --> + the rail collapses whichever is open (MainWindow code-behind). --> diff --git a/HANDOFF.md b/HANDOFF.md index e84344f..abd9b60 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,36 +1,33 @@ -# HANDOFF — 2026-09-25 (TASK 45 shipped: TEST-tab insert 400 fix — body was missing `snippet.type`) +# HANDOFF — 2026-09-25 (TASK 46 shipped: drawers close on any outside click, TEST included) ## Branch / Commit State -`main` HEAD = new commit for TASK 45 (chat-insert 400 fix); previous: TASK 44 commit -(TEST-tab chat fix), `94f0150`. Repo is ~12 commits ahead of pushed `197ee81` — -**push is the creator's call** (sub-milestone decision, not per-commit). +`main` HEAD = new commit for TASK 46 (drawer click-outside dismiss for all three tabs); +previous: TASK 45 commit (chat-insert 400 fix), `0981a72`. Repo is ~13 commits ahead of +pushed `197ee81` — **push is the creator's call** (sub-milestone decision, not per-commit). **Working tree CLEAN after this unit's commit.** -## ⚠️ The unit that just shipped (TASK 44 reopened by the live run) +## ⚠️ The unit that just shipped (creator directive) -The creator ran the TASK 44 fix and the drawer now worked — but every Mock Chat Input -send returned **"YouTube rejected the message (error 400)"**. The runtime log -(`%APPDATA%\ytLlive\startup.log`) carried the body: `400 MISSING_REQUIRED_FIELD`, -`domain: youtube.api.v3.LiveChatMessageInsertResponse.Error`. +"when the test slide-out is active, then any click off the div should close the div — +this behaviour applies to all tabs, not just TEST." -- **Root cause:** the `liveChat/messages.insert` body must declare **`snippet.type`** - (`textMessageEvent` or `pollEvent`) alongside `liveChatId` and - `textMessageDetails.messageText` — the official insert reference lists `type` as a - required property. The TASK 41 body sent only `liveChatId` + `textMessageDetails`. -- **Why the suite didn't catch it:** the TASK 41 Good Dog test asserted the POST body - contained `liveChatId` + `messageText` but **never `type`** — false-green while real - YouTube rejected every send. The fix asserts `type` too, so a dropped field fails now. -- **Fix (TASK 45, `TASKS/task-45-chat-insert-type.md`):** `InsertChatMessageAsync` adds - `type = "textMessageEvent"` to the body; test assertion strengthened; recipe added to - `MyMistakes.md` (MISSING_REQUIRED_FIELD = wrong body shape, not auth). +- **Root cause:** right-rail slides (Stream Settings / YPP / TEST, all inside + `TextPullOutHost`) close on outside clicks via `MainWindow.Window_PreviewMouseLeftButtonDown` + — but that handler only closed `BroadcastForm` + `Ypp`; **`TestSession` was never in the + close list**, so the TEST drawer ignored outside clicks. The XAML comment even said "both". +- **Fix (TASK 46, `TASKS/task-46-drawer-click-outside-close.md`):** add + `TestSession.CloseDrawerCommand` to the same `!IsClickInsideDrawer` branch; corrected + the stale "both" comment. No containment change needed — all three share the rail host. -**Good Dog:** ONE integration test — the strengthened -`TestStream_DockTooling_And_ThreeWayDrawerExclusivity` POST-body assertion. -**Gate: clean build 0 warnings; full vstest 317/318 — the 1 failure is the known -environmental `RealMouseDrag` flake (mouse stolen by a fullscreen window mid-suite); -it PASSES in isolation (3/3), confirmed just now, so our change is green.** +**Good Dog:** ONE integration test — the existing +`TestStream_DockTooling_And_ThreeWayDrawerExclusivity` section-2 window test was extended: +opens TEST and Stream Settings, raises a window-root `PreviewMouseLeftButtonDown` (source = +window ⇒ outside the rail; deterministic, no OS mouse, immune to the fullscreen mouse-steal +flake), asserts each closes. +**Gate: clean build 0 warnings; full vstest 318/318 passed** (the LayerReorder +real-mouse-drag test passed this run too). ## Follow-ups queued (NOT done in this unit) @@ -53,17 +50,16 @@ it PASSES in isolation (3/3), confirmed just now, so our change is green.** `ready`) returns nothing by design — missing liveChatId ≈ "broadcast not live yet", not an auth failure, until ~20s post push. And `400 MISSING_REQUIRED_FIELD` from `liveChat/messages.insert` = wrong body shape (missing `snippet.type`), NOT auth/scope. +- **WPF test technique (TASK 46):** a `MouseButtonEventArgs { RoutedEvent = Mouse.PreviewMouseLeftButtonDownEvent }` raised on the window root deterministically simulates an "outside the drawer rail" click — InputBindings/MouseBindings are NOT triggered by `RaiseEvent` (only by the real InputManager), so no tab toggles fire. - Every committed change needs a **close + relaunch** of the running app to be seen — running builds are live; verify can't run while the exe is locked. ## Next step -1. **Creator verifies the full TEST-tab chat path on the next run**: Test Stream → drawer - opens with a liveChatId; Mock Chat Input enabled ~as soon as the broadcast goes live; - send a message — it should now insert (200) and round-trip through the ~2s poll onto the - overlay. This completes the original creator report ("I still cannot post a chat message - in the TEST tab"). -2. Push decision is the creator's (repo ~12 ahead). If pushed, note the commit id in this file. +1. **Creator verifies the drawer behavior on the next run**: open any of Stream Settings / + YPP / TEST and click anywhere outside the rail — the drawer closes (TEST was the only + one that didn't). Clicking the vertical tab again still toggles it. +2. Push decision is the creator's (repo ~13 ahead). If pushed, note the commit id in this file. 3. When scoped: TASK 3 item 20 (RewardEvents persistence half) draws next. ## Critical working rules (unchanged, still binding) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 0e438b4..11e3751 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -143,10 +143,12 @@ public partial class MainWindow : Window if (e.OriginalSource is not DependencyObject original) return; var form = _viewModel.BroadcastForm; var ypp = _viewModel.Ypp; + var test = _viewModel.TestSession; if (!_previewPane.IsClickInsideDrawer(original)) { if (form.IsDrawerOpen) form.CloseDrawerCommand.Execute(null); if (ypp.IsDrawerOpen) ypp.CloseDrawerCommand.Execute(null); + if (test.IsDrawerOpen) test.CloseDrawerCommand.Execute(null); } if (_previewPane.IsClickInsidePreview(original)) return; if (_leftPanel.IsClickInside(original)) return; diff --git a/TASKS.md b/TASKS.md index a677cd3..f0779a4 100644 --- a/TASKS.md +++ b/TASKS.md @@ -61,6 +61,7 @@ | 43 | Native events & alerts: chat parity (all six event types + server poll cadence) + six-animation native alert box | ✅ Done (2026-09-24) | [`TASKS/task-43-native-alerts.md`](TASKS/task-43-native-alerts.md) | | 44 | TEST-tab chat fix: resolve liveChatId from snippet (not contentDetails) + poll until the broadcast is live | ✅ Done (2026-09-25); full suite 318/318 | [`TASKS/task-44-test-chat-fix.md`](TASKS/task-44-test-chat-fix.md) | | 45 | TEST-tab chat fix #2: insert body must declare `snippet.type` (400 MISSING_REQUIRED_FIELD) | ✅ Done (2026-09-25) | [`TASKS/task-45-chat-insert-type.md`](TASKS/task-45-chat-insert-type.md) | +| 46 | Drawers: click outside the rail closes whichever is open — TEST added to the existing Stream Settings + YPP dismiss behavior | ✅ Done (2026-09-25) | [`TASKS/task-46-drawer-click-outside-close.md`](TASKS/task-46-drawer-click-outside-close.md) | --- diff --git a/TASKS/task-46-drawer-click-outside-close.md b/TASKS/task-46-drawer-click-outside-close.md new file mode 100644 index 0000000..40207a7 --- /dev/null +++ b/TASKS/task-46-drawer-click-outside-close.md @@ -0,0 +1,37 @@ +# TASK 46 — Drawers: any click outside the rail closes the open drawer (TEST included) + +> Catalog: [`TASKS.md`](../TASKS.md). Status: ✅ **SHIPPED 2026-09-25**. + +## Provenance + +- **2026-09-25, creator:** "when the test slide-out is active, then any click off the + div should close the div — this behaviour applies to all tabs, not just TEST." + +## Diagnosis + +The right-rail slides (Stream Settings / YPP / TEST; `TextPullOutHost` in +`Controls/PreviewPane.xaml`) already close on any click outside the rail for Stream +Settings + YPP — `MainWindow.Window_PreviewMouseLeftButtonDown` closes +`BroadcastForm` and `Ypp` when `IsClickInsideDrawer` is false. The handler simply +**omitted `TestSession`**, so the TEST drawer never dismissed on an outside click. +The XAML comment mentioned "both" — stale once a third drawer landed. + +## What shipped + +- `MainWindow.xaml.cs` — the outside-click close now also runs + `TestSession.CloseDrawerCommand` when the TEST drawer is open (matching the other + two; all three share `TextPullOutHost`, so the containment check is unchanged). +- `Controls/PreviewPane.xaml` — comment corrected ("the rail … whichever is open"). +- `ytLive.Tests/TestStreamTests.cs` — section-2 window test now opens TEST and Stream + Settings, raises a window-root `PreviewMouseLeftButtonDown` (sourced on the window, + i.e. outside the rail — deterministic, no OS mouse input, immune to the fullscreen + mouse-steal flake), and asserts each closes. +- Docs: `ai.md` (Test Stream section), `TASKS.md` (row 46), `HANDOFF.md` rewritten. + +## Validation (Good Dog: ONE integration test) + +`TestStream_DockTooling_And_ThreeWayDrawerExclusivity` — extended section 2, no new +tests: the raised outside-click asserts TEST (the fixed drawer) AND Stream Settings +(the pre-existing behavior) both close. + +**Gate: clean build 0 warnings; full vstest (captured in HANDOFF).** \ No newline at end of file diff --git a/ai.md b/ai.md index 73722df..fc87ec0 100644 --- a/ai.md +++ b/ai.md @@ -1559,7 +1559,10 @@ These are the hard facts behind every decision. Full list in `TASKS.md`. gold top bar + glow (`#9c6f1c`), a **TEST** badge, and the End button reads **"End Test"**; `StopStream` clears it in every path (including pump-failure rollback). Once the liveChatId resolves, `TestSession.BeginTest(broadcastId, liveChatId)` opens the **TEST drawer** (third - right-rail pull-out, three-way one-open-at-a-time exclusivity wired in `MainViewModel.Ypp.cs`). + right-rail pull-out, three-way one-open-at-a-time exclusivity wired in `MainViewModel.Ypp.cs`; + any left-click outside the rail closes whichever drawer is open — TEST included (fixed + 2026-09-25, TASK 46; the code-behind outside-click close in `MainWindow` had skipped + TestSession, so only Stream Settings + YPP dismissed on an outside click). **liveChatId resolution (fixed 2026-09-25, TASK 44):** the id is `snippet.liveChatId` — `contentDetails` has NO liveChatId property — and YouTube only populates it once the broadcast is LIVE (the official GetLiveChatId.java sample lists broadcastStatus=active). So diff --git a/ytLive.Tests/TestStreamTests.cs b/ytLive.Tests/TestStreamTests.cs index 1fbf154..9b199a8 100644 --- a/ytLive.Tests/TestStreamTests.cs +++ b/ytLive.Tests/TestStreamTests.cs @@ -6,6 +6,8 @@ using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; using Microsoft.Data.Sqlite; using Xunit; using ytLive.Models; @@ -183,6 +185,24 @@ public sealed class TestStreamTests Assert.True(model.TestSession.IsDrawerOpen); model.TestSession.CloseDrawerCommand.Execute(null); Assert.False(model.TestSession.IsDrawerOpen); + + // Any left-click OUTSIDE the drawer rail collapses whichever drawer is + // open (MainWindow.Window_PreviewMouseLeftButtonDown) — the TEST drawer + // was the missing third; Stream Settings + YPP already behaved this way. + // A raised PreviewMouseLeftButtonDown on the window root lands "outside", + // deterministically, with no OS mouse input involved. + var outsideClick = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left) + { + RoutedEvent = UIElement.PreviewMouseLeftButtonDownEvent, + }; + model.TestSession.ToggleDrawerCommand.Execute(null); + Assert.True(model.TestSession.IsDrawerOpen); + window.RaiseEvent(outsideClick); + Assert.False(model.TestSession.IsDrawerOpen, "TEST drawer must close on an outside click"); + model.BroadcastForm.ToggleDrawerCommand.Execute(null); + Assert.True(model.BroadcastForm.IsDrawerOpen); + window.RaiseEvent(outsideClick); + Assert.False(model.BroadcastForm.IsDrawerOpen, "Stream Settings drawer must close on an outside click"); } finally {