fix(ui): drawers close on any click outside the rail — TEST was the missing third
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.
This commit is contained in:
@@ -418,7 +418,7 @@
|
|||||||
edge; each toggles its own drawer (the same animated-width pattern).
|
edge; each toggles its own drawer (the same animated-width pattern).
|
||||||
Only one drawer is open at a time — IsDrawerOpen exclusivity is
|
Only one drawer is open at a time — IsDrawerOpen exclusivity is
|
||||||
VM-enforced (opening one collapses the other). Any click outside
|
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). -->
|
||||||
<Grid Grid.Row="0">
|
<Grid Grid.Row="0">
|
||||||
<StackPanel x:Name="TextPullOutHost" Orientation="Horizontal"
|
<StackPanel x:Name="TextPullOutHost" Orientation="Horizontal"
|
||||||
HorizontalAlignment="Right" VerticalAlignment="Stretch">
|
HorizontalAlignment="Right" VerticalAlignment="Stretch">
|
||||||
|
|||||||
+26
-30
@@ -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
|
## Branch / Commit State
|
||||||
|
|
||||||
`main` HEAD = new commit for TASK 45 (chat-insert 400 fix); previous: TASK 44 commit
|
`main` HEAD = new commit for TASK 46 (drawer click-outside dismiss for all three tabs);
|
||||||
(TEST-tab chat fix), `94f0150`. Repo is ~12 commits ahead of pushed `197ee81` —
|
previous: TASK 45 commit (chat-insert 400 fix), `0981a72`. Repo is ~13 commits ahead of
|
||||||
**push is the creator's call** (sub-milestone decision, not per-commit).
|
pushed `197ee81` — **push is the creator's call** (sub-milestone decision, not per-commit).
|
||||||
|
|
||||||
**Working tree CLEAN after this unit's 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
|
"when the test slide-out is active, then any click off the div should close the div —
|
||||||
send returned **"YouTube rejected the message (error 400)"**. The runtime log
|
this behaviour applies to all tabs, not just TEST."
|
||||||
(`%APPDATA%\ytLlive\startup.log`) carried the body: `400 MISSING_REQUIRED_FIELD`,
|
|
||||||
`domain: youtube.api.v3.LiveChatMessageInsertResponse.Error`.
|
|
||||||
|
|
||||||
- **Root cause:** the `liveChat/messages.insert` body must declare **`snippet.type`**
|
- **Root cause:** right-rail slides (Stream Settings / YPP / TEST, all inside
|
||||||
(`textMessageEvent` or `pollEvent`) alongside `liveChatId` and
|
`TextPullOutHost`) close on outside clicks via `MainWindow.Window_PreviewMouseLeftButtonDown`
|
||||||
`textMessageDetails.messageText` — the official insert reference lists `type` as a
|
— but that handler only closed `BroadcastForm` + `Ypp`; **`TestSession` was never in the
|
||||||
required property. The TASK 41 body sent only `liveChatId` + `textMessageDetails`.
|
close list**, so the TEST drawer ignored outside clicks. The XAML comment even said "both".
|
||||||
- **Why the suite didn't catch it:** the TASK 41 Good Dog test asserted the POST body
|
- **Fix (TASK 46, `TASKS/task-46-drawer-click-outside-close.md`):** add
|
||||||
contained `liveChatId` + `messageText` but **never `type`** — false-green while real
|
`TestSession.CloseDrawerCommand` to the same `!IsClickInsideDrawer` branch; corrected
|
||||||
YouTube rejected every send. The fix asserts `type` too, so a dropped field fails now.
|
the stale "both" comment. No containment change needed — all three share the rail host.
|
||||||
- **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).
|
|
||||||
|
|
||||||
**Good Dog:** ONE integration test — the strengthened
|
**Good Dog:** ONE integration test — the existing
|
||||||
`TestStream_DockTooling_And_ThreeWayDrawerExclusivity` POST-body assertion.
|
`TestStream_DockTooling_And_ThreeWayDrawerExclusivity` section-2 window test was extended:
|
||||||
**Gate: clean build 0 warnings; full vstest 317/318 — the 1 failure is the known
|
opens TEST and Stream Settings, raises a window-root `PreviewMouseLeftButtonDown` (source =
|
||||||
environmental `RealMouseDrag` flake (mouse stolen by a fullscreen window mid-suite);
|
window ⇒ outside the rail; deterministic, no OS mouse, immune to the fullscreen mouse-steal
|
||||||
it PASSES in isolation (3/3), confirmed just now, so our change is green.**
|
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)
|
## 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
|
`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
|
auth failure, until ~20s post push. And `400 MISSING_REQUIRED_FIELD` from
|
||||||
`liveChat/messages.insert` = wrong body shape (missing `snippet.type`), NOT auth/scope.
|
`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
|
- 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.
|
builds are live; verify can't run while the exe is locked.
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
1. **Creator verifies the full TEST-tab chat path on the next run**: Test Stream → drawer
|
1. **Creator verifies the drawer behavior on the next run**: open any of Stream Settings /
|
||||||
opens with a liveChatId; Mock Chat Input enabled ~as soon as the broadcast goes live;
|
YPP / TEST and click anywhere outside the rail — the drawer closes (TEST was the only
|
||||||
send a message — it should now insert (200) and round-trip through the ~2s poll onto the
|
one that didn't). Clicking the vertical tab again still toggles it.
|
||||||
overlay. This completes the original creator report ("I still cannot post a chat message
|
2. Push decision is the creator's (repo ~13 ahead). If pushed, note the commit id in this file.
|
||||||
in the TEST tab").
|
|
||||||
2. Push decision is the creator's (repo ~12 ahead). If pushed, note the commit id in this file.
|
|
||||||
3. When scoped: TASK 3 item 20 (RewardEvents persistence half) draws next.
|
3. When scoped: TASK 3 item 20 (RewardEvents persistence half) draws next.
|
||||||
|
|
||||||
## Critical working rules (unchanged, still binding)
|
## Critical working rules (unchanged, still binding)
|
||||||
|
|||||||
@@ -143,10 +143,12 @@ public partial class MainWindow : Window
|
|||||||
if (e.OriginalSource is not DependencyObject original) return;
|
if (e.OriginalSource is not DependencyObject original) return;
|
||||||
var form = _viewModel.BroadcastForm;
|
var form = _viewModel.BroadcastForm;
|
||||||
var ypp = _viewModel.Ypp;
|
var ypp = _viewModel.Ypp;
|
||||||
|
var test = _viewModel.TestSession;
|
||||||
if (!_previewPane.IsClickInsideDrawer(original))
|
if (!_previewPane.IsClickInsideDrawer(original))
|
||||||
{
|
{
|
||||||
if (form.IsDrawerOpen) form.CloseDrawerCommand.Execute(null);
|
if (form.IsDrawerOpen) form.CloseDrawerCommand.Execute(null);
|
||||||
if (ypp.IsDrawerOpen) ypp.CloseDrawerCommand.Execute(null);
|
if (ypp.IsDrawerOpen) ypp.CloseDrawerCommand.Execute(null);
|
||||||
|
if (test.IsDrawerOpen) test.CloseDrawerCommand.Execute(null);
|
||||||
}
|
}
|
||||||
if (_previewPane.IsClickInsidePreview(original)) return;
|
if (_previewPane.IsClickInsidePreview(original)) return;
|
||||||
if (_leftPanel.IsClickInside(original)) return;
|
if (_leftPanel.IsClickInside(original)) return;
|
||||||
|
|||||||
@@ -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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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).**
|
||||||
@@ -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"**;
|
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
|
`StopStream` clears it in every path (including pump-failure rollback). Once the liveChatId
|
||||||
resolves, `TestSession.BeginTest(broadcastId, liveChatId)` opens the **TEST drawer** (third
|
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` —
|
**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
|
`contentDetails` has NO liveChatId property — and YouTube only populates it once the broadcast
|
||||||
is LIVE (the official GetLiveChatId.java sample lists broadcastStatus=active). So
|
is LIVE (the official GetLiveChatId.java sample lists broadcastStatus=active). So
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using ytLive.Models;
|
using ytLive.Models;
|
||||||
@@ -183,6 +185,24 @@ public sealed class TestStreamTests
|
|||||||
Assert.True(model.TestSession.IsDrawerOpen);
|
Assert.True(model.TestSession.IsDrawerOpen);
|
||||||
model.TestSession.CloseDrawerCommand.Execute(null);
|
model.TestSession.CloseDrawerCommand.Execute(null);
|
||||||
Assert.False(model.TestSession.IsDrawerOpen);
|
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
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user