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
+75 -34
View File
@@ -71,13 +71,14 @@
only the inactive half. -->
<Border Background="{StaticResource Brush.SegmentTrack}" CornerRadius="12" Padding="4"
VerticalAlignment="Center" Margin="0,0,12,0"
KeyboardNavigation.TabNavigation="Cycle" KeyboardNavigation.DirectionalNavigation="Cycle"
Visibility="{Binding IsBuildWorld, Converter={StaticResource BoolToVis}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Style="{StaticResource SegmentHalf}"
<Button x:Name="BuildSegmentButton" Grid.Column="0" Style="{StaticResource SegmentHalf}"
Tag="{Binding IsBuildWorld}" Command="{Binding EnterBuildWorldCommand}"
ToolTip="Edit layers, scenes and webcam placement">
<StackPanel Orientation="Horizontal">
@@ -88,13 +89,13 @@
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
</Button>
<Button Grid.Column="1" Style="{StaticResource SegmentHalf}"
<Button x:Name="OutputSegmentButton" Grid.Column="1" Style="{StaticResource SegmentHalf}"
Tag="{Binding IsOutputWorld}" Command="{Binding EnterOutputWorldCommand}"
ToolTip="Choose the output and start the session">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource SegmentIcon}" Tag="{Binding IsOutputWorld}"
Data="M12,2a10,10 0 1,0 0,20a10,10 0 1,0 0,-20zM12,4a8,8 0 1,1 0,16a8,8 0 1,1 0,-16zM12,7.5a4.5,4.5 0 1,1 0,9a4.5,4.5 0 1,1 0,-9z"/>
<TextBlock Text="Record / go live" FontSize="12" Margin="6,0,0,0"
<TextBlock Text="Record / Go-Live" FontSize="12" Margin="6,0,0,0"
VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
@@ -104,42 +105,82 @@
<!-- IDLE: the mode switch + the action(s) of the armed world. The three
controls share one height (build 77 feedback) so they read as one
family — segments host, Start/Go Live, and Test all sit at 32. -->
<!-- THE OUTPUT LINE (creator ruling 2026-11-12): which single output. Nothing else
belongs in it — the action is not part of this row. -->
<!-- THE OUTPUT STAGE (creator, builds 728/729). Three states:
1. nothing chosen: [YouTube] [Local MP4]
2. YouTube chosen: [YouTube(red)] [Live Stream] [Private Stream]
3. Local MP4 chosen: [Local MP4(red)] [Start Recording] -> [End Recording]
"Private Stream" is the private-visibility stream, which is what this app has always
called the test stream. The action chips are never highlighted; only the chosen output
is red. -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
Visibility="{Binding ShowOutputStage, Converter={StaticResource BoolToVis}}">
<TextBlock Text="Output:" FontSize="11" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}" Margin="0,0,8,0"/>
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Border Background="{StaticResource Brush.SurfaceContainerLowest}" CornerRadius="4" Padding="2" Height="32" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<ToggleButton Style="{StaticResource RecordPillToggle}"
IsChecked="{Binding OnAirPillOn}">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Stretch="Uniform" VerticalAlignment="Center"
Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"
Data="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
<TextBlock Text="YouTube" FontSize="12" FontWeight="Bold" Margin="6,0,0,0"
VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"/>
</StackPanel>
</ToggleButton>
<ToggleButton Style="{StaticResource RecordPillToggle}"
IsChecked="{Binding RecordPillOn}">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Stretch="Uniform" VerticalAlignment="Center"
Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"
Data="M12,2a10,10 0 1,0 0,20a10,10 0 1,0 0,-20zM12,4a8,8 0 1,1 0,16a8,8 0 1,1 0,-16zM12,7.5a4.5,4.5 0 1,1 0,9a4.5,4.5 0 1,1 0,-9z"/>
<TextBlock Text="Local MP4" FontSize="12" FontWeight="Bold" Margin="6,0,0,0"
VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"/>
</StackPanel>
</ToggleButton>
</StackPanel>
</Border>
<!-- Esc is the way back now that the chevron is gone (creator ruling 2026-11-12). -->
<TextBlock Text="press &lt;esc&gt; to return" FontSize="10" HorizontalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}" Margin="0,2,0,0"/>
<Border Background="{StaticResource Brush.SegmentTrack}" CornerRadius="12" Padding="4"
VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<!-- The chosen output, or the two choices while none is chosen. -->
<Button Style="{StaticResource OutputChip}" Tag="{Binding OnAirPillOn}"
Command="{Binding SelectYouTubeCommand}"
Visibility="{Binding YouTubeChipVisible, Converter={StaticResource BoolToVis}}"
ToolTip="Stream to YouTube">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Tag="{Binding OnAirPillOn}" Data="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
<TextBlock Text="YouTube" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
</Button>
<Button Style="{StaticResource OutputChip}" Tag="{Binding RecordPillOn}"
Command="{Binding SelectLocalMp4Command}"
Visibility="{Binding LocalMp4ChipVisible, Converter={StaticResource BoolToVis}}"
ToolTip="Record to a local MP4">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Tag="{Binding RecordPillOn}" Data="M12,2a10,10 0 1,0 0,20a10,10 0 1,0 0,-20zM12,4a8,8 0 1,1 0,16a8,8 0 1,1 0,-16zM12,7.5a4.5,4.5 0 1,1 0,9a4.5,4.5 0 1,1 0,-9z"/>
<TextBlock Text="Local MP4" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
</Button>
<!-- YouTube's two actions: the real stream and the private (test) one. -->
<Button Style="{StaticResource OutputChip}" Command="{Binding GoLiveCommand}"
Visibility="{Binding ShowYouTubeSelected, Converter={StaticResource BoolToVis}}"
ToolTip="Go live on YouTube">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Data="M8,5v14l11,-7z"/>
<TextBlock Text="Live Stream" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
Foreground="{StaticResource Brush.SegmentTextInactive}"/>
</StackPanel>
</Button>
<Button Style="{StaticResource OutputChip}" Command="{Binding TestStreamCommand}"
Visibility="{Binding ShowYouTubeSelected, Converter={StaticResource BoolToVis}}"
ToolTip="Private test stream — nothing public is published">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Data="M7,10V7a5,5 0 0,1 10,0v3h1a1,1 0 0,1 1,1v9a1,1 0 0,1 -1,1H6a1,1 0 0,1 -1,-1v-9a1,1 0 0,1 1,-1h1zM9,10h6V7a3,3 0 0,0 -6,0v3z"/>
<TextBlock Text="Private Stream" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
Foreground="{StaticResource Brush.SegmentTextInactive}"/>
</StackPanel>
</Button>
<!-- Local MP4's one action, which becomes End while it records. -->
<Button Style="{StaticResource OutputChip}" Command="{Binding StartRecordingCommand}"
Visibility="{Binding ShowStartRecordingAction, Converter={StaticResource BoolToVis}}">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Data="M12,2a10,10 0 1,0 0,20a10,10 0 1,0 0,-20zM12,4a8,8 0 1,1 0,16a8,8 0 1,1 0,-16zM12,7.5a4.5,4.5 0 1,1 0,9a4.5,4.5 0 1,1 0,-9z"/>
<TextBlock Text="Start Recording" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
Foreground="{StaticResource Brush.SegmentTextInactive}"/>
</StackPanel>
</Button>
<Button Style="{StaticResource OutputChip}" Command="{Binding EndStreamCommand}"
Visibility="{Binding ShowEndRecordingAction, Converter={StaticResource BoolToVis}}">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Data="M6,6h12v12H6z"/>
<TextBlock Text="End Recording" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
Foreground="{StaticResource Brush.SegmentTextInactive}"/>
</StackPanel>
</Button>
</StackPanel>
</Border>
<TextBlock Text="&lt;esc&gt; to return" FontSize="10" HorizontalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}" Margin="0,2,0,0"/>
</StackPanel>
<TextBlock Text="One output at a time" FontSize="11" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}" Margin="8,0,0,0"/>
+4
View File
@@ -19,6 +19,10 @@ public partial class TopBar : UserControl
InitializeComponent();
AvatarImage.Source = null;
AvatarInitial.Visibility = System.Windows.Visibility.Collapsed;
// Keyboard enters the bar on Build scenes, and Tab cycles between the two halves only
// (creator, build 729) — Tab must not walk off into the rest of the window from here.
Loaded += (_, _) => BuildSegmentButton.Focus();
}
/// <summary>Right-click on the LlamaCasty wordmark opens the app menu (Report Bug / Feature
+48
View File
@@ -1002,6 +1002,54 @@
<!-- REC / ON-AIR segment pills: an armed session control reads red, because red is what
"live or recording" means (DESIGN.md). Kept separate from SegmentToggle so the Rect|Circle
choice in the properties panel stays the interactive blue. -->
<!-- An output chip: muted until it is the CHOSEN output, then red with white ink (creator, build
729). Tag carries that chip's own selection flag, so one style serves every chip. -->
<Style x:Key="OutputChip" TargetType="Button">
<Setter Property="Background" Value="{StaticResource Brush.SegmentHitArea}"/>
<Setter Property="Foreground" Value="{StaticResource Brush.SegmentTextInactive}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Margin" Value="0,0,4,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Face" Background="{TemplateBinding Background}"
CornerRadius="8" Padding="12,4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Background" Value="{StaticResource Brush.Primary}"/>
<Setter Property="Foreground" Value="{StaticResource Brush.OnAccent}"/>
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True"/>
<Condition Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="{StaticResource Brush.SegmentHover}"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
<!-- The chip glyph follows the chip: muted normally, white on the red selected face. -->
<Style x:Key="OutputChipIcon" TargetType="Path">
<Setter Property="Fill" Value="{StaticResource Brush.SegmentTextInactive}"/>
<Setter Property="Width" Value="14"/>
<Setter Property="Height" Value="14"/>
<Setter Property="Stretch" Value="Uniform"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Fill" Value="{StaticResource Brush.OnAccent}"/>
</DataTrigger>
</Style.Triggers>
</Style>
<!-- The segmented mode switcher's icon: muted when its segment is inactive, coral when active.
Tag carries the segment's own flag so one style serves both halves (creator mock 2026-11-12). -->
<Style x:Key="SegmentIcon" TargetType="Path">
+2
View File
@@ -117,6 +117,7 @@ public partial class MainViewModel : ViewModelBase
{
if (!SetProperty(ref _recordPillOn, value)) return;
if (value && _onAirPillOn) OnAirPillOn = false; // record-OR-live — arming REC drops ON-AIR
RaiseOutputStage();
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(PrimaryStartKind));
OnPropertyChanged(nameof(PrimaryStartButtonLabel));
@@ -136,6 +137,7 @@ public partial class MainViewModel : ViewModelBase
// intent, not reality; the Start face turns this into a sign-in.
if (!SetProperty(ref _onAirPillOn, value)) return;
if (value && _recordPillOn) RecordPillOn = false; // record-OR-live — arming ON-AIR drops REC
RaiseOutputStage();
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(PrimaryStartKind));
OnPropertyChanged(nameof(PrimaryStartButtonLabel));
+44 -8
View File
@@ -35,11 +35,23 @@ public partial class MainViewModel
/// running — a live session cannot change its output, so its switch is not offered.</summary>
public bool ShowOutputStage => IsOutputWorld && IsOffline && !IsRecording;
/// <summary>The action the armed output offers: recording is one button, YouTube is two — the test
/// run and the real one are separate asks since 2026-11-12, not something inferred from EDIT.</summary>
public bool ShowStartRecordingAction => ShowOutputStage && RecordPillOn;
public bool ShowYouTubeStartActions => ShowOutputStage && OnAirPillOn;
// ─── The output stage's three states (creator, builds 728/729) ───
// 1. nothing chosen: [YouTube] [Local MP4]
// 2. YouTube chosen: [YouTube(red)] [Live Stream] [Private Stream] (Private = the test run)
// 3. Local MP4 chosen: [Local MP4(red)] [Start Recording] -> [End Recording] while recording
public bool ShowOutputChoice => ShowOutputStage && !RecordPillOn && !OnAirPillOn;
public bool ShowYouTubeSelected => ShowOutputStage && OnAirPillOn;
public bool ShowLocalMp4Selected => ShowOutputStage && RecordPillOn;
/// <summary>The YouTube chip shows while nothing is chosen and while YouTube IS the choice; once
/// Local MP4 is chosen the MP4 chip replaces it in the left slot (creator, build 729).</summary>
public bool YouTubeChipVisible => ShowOutputChoice || ShowYouTubeSelected;
public bool LocalMp4ChipVisible => ShowOutputChoice || ShowLocalMp4Selected;
public bool ShowStartRecordingAction => ShowLocalMp4Selected && !IsRecording;
public bool ShowEndRecordingAction => ShowLocalMp4Selected && IsRecording;
public ICommand SelectYouTubeCommand { get; private set; } = null!;
public ICommand SelectLocalMp4Command { get; private set; } = null!;
public ICommand StartRecordingCommand { get; private set; } = null!;
public ICommand TestStreamCommand { get; private set; } = null!;
public ICommand GoLiveCommand { get; private set; } = null!;
@@ -49,34 +61,58 @@ public partial class MainViewModel
private void InitializeWorldCommands()
{
SelectYouTubeCommand = new RelayCommand(_ => OnAirPillOn = true, _ => ShowOutputChoice);
SelectLocalMp4Command = new RelayCommand(_ => RecordPillOn = true, _ => ShowOutputChoice);
StartRecordingCommand = new RelayCommand(_ => BeginRecordOnly(), _ => ShowStartRecordingAction);
// A signed-out press starts the sign-in flow instead of sitting dead (no-dead-ends rule).
TestStreamCommand = new RelayCommand(_ =>
{
if (!IsConnected) { _ = SignInAsync(); return; }
BeginTestStream();
}, _ => ShowYouTubeStartActions);
}, _ => ShowYouTubeSelected);
GoLiveCommand = new RelayCommand(_ =>
{
if (!IsConnected) { _ = SignInAsync(); return; }
StartSession();
}, _ => ShowYouTubeStartActions);
}, _ => ShowYouTubeSelected);
EnterBuildWorldCommand = new RelayCommand(_ => EnterWorld(BarWorld.Build));
EnterOutputWorldCommand = new RelayCommand(_ => EnterWorld(BarWorld.Output));
}
/// <summary>Everything the output stage shows or hides, in one place so no caller can forget
/// one of them.</summary>
internal void RaiseOutputStage()
{
OnPropertyChanged(nameof(ShowOutputStage));
OnPropertyChanged(nameof(ShowOutputChoice));
OnPropertyChanged(nameof(ShowYouTubeSelected));
OnPropertyChanged(nameof(YouTubeChipVisible));
OnPropertyChanged(nameof(LocalMp4ChipVisible));
OnPropertyChanged(nameof(ShowLocalMp4Selected));
OnPropertyChanged(nameof(ShowStartRecordingAction));
OnPropertyChanged(nameof(ShowEndRecordingAction));
}
/// <summary>Switches worlds. Re-entering the current one still re-asserts its edit state, which is
/// what lets a creator take EDIT back mid-show by clicking Build Scenes.</summary>
internal void EnterWorld(BarWorld world)
{
_barWorld = world;
// Entering a world starts with NOTHING chosen inside it (creator, build 729): the output
// switches are a fresh decision each time, not a remembered one.
if (world == BarWorld.Output)
{
RecordPillOn = false;
OnAirPillOn = false;
}
OnPropertyChanged(nameof(BarWorld));
OnPropertyChanged(nameof(IsBuildWorld));
OnPropertyChanged(nameof(IsOutputWorld));
EditingActive = world == BarWorld.Build;
OnPropertyChanged(nameof(ShowOutputStage));
OnPropertyChanged(nameof(ShowStartRecordingAction));
OnPropertyChanged(nameof(ShowYouTubeStartActions));
RaiseOutputStage();
}
}
+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);