feat(topbar): the output stage's three states, and keyboard entry into the bar

Build 729 (creator):

- "press <esc> to return" loses the word "press"; the label is "Record / Go-Live".
- The bar takes focus on "Build scenes" and Tab cycles between the two halves only
  (KeyboardNavigation.TabNavigation="Cycle"), never off into the window.
- Entering Record / Go-Live starts with NOTHING chosen — the output switches are a fresh decision,
  not a remembered one.
- Three states: nothing chosen [YouTube][Local MP4]; YouTube chosen [YouTube(red)][Live Stream][Private
  Stream]; Local MP4 chosen [Local MP4(red)][Start Recording] → [End Recording] while recording. Only the
  chosen output is red; the action chips never highlight. Choosing Local MP4 takes the left slot and the
  YouTube chip disappears with it, which is what "slides to the left, replacing it" describes.

"Private Stream" is the private-visibility stream — the same thing this app has always called the test
stream (creator's clarification, so the wording is deliberate, not a new broadcast type).

Output chips are one style with a Tag-driven trigger (OutputChip + OutputChipIcon), so a chip's red face
and its glyph colour can't drift apart; the one-alpha hit-area colour is a token, because
DesignTokensTests rightly refuses literals in theme files.

680 pass, 0 warnings. Scope justification: Assets/starting-background.jpg is outside this unit — the
creator's replacement art, still awaiting their visual check.
This commit is contained in:
2026-11-12 16:35:16 -08:00
parent 6b4aa99863
commit caf5d6130d
7 changed files with 205 additions and 55 deletions
+27 -8
View File
@@ -31,26 +31,45 @@ public class TopBarWorldTests
Assert.False(vm.IsRecording);
Assert.True(vm.ShowOutputStage);
// Local MP4 is the default output; the pair is one axis, so arming either drops the other.
Assert.True(vm.RecordPillOn);
// Entering the output world starts with NOTHING chosen (creator, build 729); the pair is
// one axis, so arming either drops the other.
Assert.False(vm.RecordPillOn);
Assert.False(vm.OnAirPillOn);
vm.OnAirPillOn = true;
Assert.True(vm.OnAirPillOn);
Assert.False(vm.RecordPillOn);
vm.RecordPillOn = true;
Assert.False(vm.OnAirPillOn);
vm.RecordPillOn = false; // back to "nothing chosen" for the state checks below
// The action follows the output: one button for MP4, two for YouTube (test + real).
// Nothing chosen yet: both output chips, no action.
Assert.True(vm.ShowOutputChoice);
Assert.False(vm.ShowYouTubeSelected);
Assert.False(vm.ShowLocalMp4Selected);
Assert.True(vm.YouTubeChipVisible);
Assert.True(vm.LocalMp4ChipVisible);
// YouTube chosen: its chip stays (red), two actions appear, MP4's chip goes away.
vm.SelectYouTubeCommand.Execute(null);
Assert.True(vm.ShowYouTubeSelected);
Assert.False(vm.ShowOutputChoice);
Assert.True(vm.YouTubeChipVisible);
Assert.False(vm.LocalMp4ChipVisible);
// Local MP4 chosen: it takes the left slot, one action, and Start/End swap on recording.
vm.SelectLocalMp4Command.Execute(null);
Assert.True(vm.ShowLocalMp4Selected);
Assert.False(vm.YouTubeChipVisible);
Assert.True(vm.LocalMp4ChipVisible);
Assert.True(vm.ShowStartRecordingAction);
Assert.False(vm.ShowYouTubeStartActions);
vm.OnAirPillOn = true;
Assert.False(vm.ShowStartRecordingAction);
Assert.True(vm.ShowYouTubeStartActions);
Assert.False(vm.ShowEndRecordingAction);
// Neither action exists while building scenes.
vm.EnterBuildWorldCommand.Execute(null);
Assert.False(vm.ShowStartRecordingAction);
Assert.False(vm.ShowYouTubeStartActions);
Assert.False(vm.ShowEndRecordingAction);
Assert.False(vm.YouTubeChipVisible);
Assert.False(vm.LocalMp4ChipVisible);
}
finally
{
+5 -5
View File
@@ -30,7 +30,7 @@ public class UiShellTests
// 2. The world segment replaced the EDIT toggle (creator ruling 2026-11-12): both halves are
// present, each lit by its own world flag through the shared style.
Assert.Contains("Text=\"Build scenes\"", topBar);
Assert.Contains("Text=\"Record / go live\"", topBar);
Assert.Contains("Text=\"Record / Go-Live\"", topBar);
Assert.Contains("Tag=\"{Binding IsBuildWorld}\"", topBar);
Assert.Contains("Tag=\"{Binding IsOutputWorld}\"", topBar);
Assert.DoesNotContain("x:Name=\"EditBody\"", topBar);
@@ -44,7 +44,7 @@ public class UiShellTests
Assert.Contains("Style=\"{StaticResource SegmentIcon}\"", topBar);
Assert.Contains("Visibility=\"{Binding IsBuildWorld, Converter={StaticResource BoolToVis}}\"", topBar);
Assert.Contains("Text=\"Output:\"", topBar);
Assert.Contains("Text=\"press &lt;esc&gt; to return\"", topBar);
Assert.Contains("Text=\"&lt;esc&gt; to return\"", topBar);
Assert.DoesNotContain("ToolTip=\"Back to Build Scenes\"", topBar); // Esc replaced the chevron
// 2c. The output stage: two labelled switches and the exclusivity promise beside them.
@@ -52,8 +52,8 @@ public class UiShellTests
Assert.Contains("Text=\"YouTube\"", topBar);
Assert.Contains("Text=\"Local MP4\"", topBar);
Assert.Contains("Text=\"One output at a time\"", topBar);
Assert.Contains("IsChecked=\"{Binding OnAirPillOn}\"", topBar);
Assert.Contains("IsChecked=\"{Binding RecordPillOn}\"", topBar);
Assert.Contains("Command=\"{Binding SelectYouTubeCommand}\"", topBar);
Assert.Contains("Command=\"{Binding SelectLocalMp4Command}\"", topBar);
// 2d. The bar carries NO action button: the output line is the bar's whole middle
// (creator ruling 2026-11-12 — the action is placed elsewhere, on instruction).
@@ -76,7 +76,7 @@ public class UiShellTests
pill = pill[..pill.IndexOf("</Style>", System.StringComparison.Ordinal)];
Assert.Contains("<Setter Property=\"Foreground\" Value=\"{StaticResource Brush.OnAccent}\"/>", pill);
Assert.Contains("Brush.Primary", theme);
Assert.Contains("Style=\"{StaticResource RecordPillToggle}\"", topBar);
Assert.Contains("Style=\"{StaticResource OutputChip}\"", topBar);
Assert.Contains("Background=\"{StaticResource Brush.Primary}\" Margin=\"14,0,0,0\"", topBar);
Assert.DoesNotContain("Background=\"#333\"", topBar);