feat(topbar): three activities, always visible — build, stream, record
Creator ruling 2026-11-13: "the app does three things. builds. records. live-streams." So the bar holds three buttons that never disappear; the active one is lit and runs its own flow in place, and the other two dim but stay clickable, so there is no state where the creator cannot see where they are or how to step back. Build [Build Scenes] -> editing on, nothing to start YouTube [Live][Private] -> [Start] -> [Stop] (Private = the private-visibility test stream) Record [Start Recording] -> [● REC hh:mm:ss][Stop Recording] Arming and starting are separate presses on purpose (creator): Start is the last chance to tell someone "check you are ready". Collapsing them would save one click and lose that prompt. This replaces the two-world model (BarWorld) and its output stage outright, including the chips added earlier today. The old pills remain the underlying state (OnAirPillOn/RecordPillOn) so the start, stop and step-down paths are untouched; the activity only decides what the bar shows. Closing the app no longer waits 6 s for a session that cannot stop: it waits ~2 s, then clears the state and closes anyway. A window that refuses to close is worse than a stale flag — and that refusal is what hung the suite in ZoomBoxInteractionTests, which sets IsRecording and closes a real window. Test: TopBarActivityTests pins all three flows, the arm-then-start split, and that stepping back to Build forgets the YouTube choice instead of carrying it. TopBarWorldTests is retired with the model it tested.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Three activities, always visible (creator, 2026-11-13): build, stream, record. The active one is lit
|
||||
/// and runs its own flow in place; the other two stay on the bar and dim, so there is no state where the
|
||||
/// creator cannot see where they are or how to step back.
|
||||
/// </summary>
|
||||
[Collection("MainViewModelLayoutPath")]
|
||||
public class TopBarActivityTests
|
||||
{
|
||||
private static MainViewModel NewVm(string db)
|
||||
{
|
||||
MainViewModel.LayoutPathOverride = db;
|
||||
return new MainViewModel();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Three_activities_stay_on_the_bar_and_the_active_one_owns_its_flow()
|
||||
{
|
||||
var db = Path.Combine(Path.GetTempPath(), $"llamacasty-activity-{Guid.NewGuid():N}.db");
|
||||
try
|
||||
{
|
||||
var vm = NewVm(db);
|
||||
|
||||
// Launch: Build is the activity and the only one editing.
|
||||
Assert.Equal(TopActivity.Build, vm.Activity);
|
||||
Assert.True(vm.IsBuildActivity);
|
||||
Assert.True(vm.IsEditMode);
|
||||
Assert.False(vm.IsRecording);
|
||||
|
||||
// Record: its own flow becomes Start, and editing steps aside.
|
||||
vm.SelectRecordActivityCommand.Execute(null);
|
||||
Assert.True(vm.IsRecordActivity);
|
||||
Assert.False(vm.IsEditMode);
|
||||
Assert.True(vm.ShowRecordStart);
|
||||
Assert.False(vm.ShowRecordStop);
|
||||
Assert.Equal("Start Recording", vm.RecordActionLabel);
|
||||
|
||||
// Recording swaps the flow to Stop, and the elapsed label carries REC.
|
||||
vm.IsRecording = true;
|
||||
Assert.False(vm.ShowRecordStart);
|
||||
Assert.True(vm.ShowRecordStop);
|
||||
Assert.Equal("Stop Recording", vm.RecordActionLabel);
|
||||
Assert.StartsWith("REC: ", vm.RecordElapsedLabel);
|
||||
vm.IsRecording = false;
|
||||
|
||||
// YouTube: choosing Live only ARMS it. Start is a second press — the "are you ready?" moment.
|
||||
vm.SelectYouTubeActivityCommand.Execute(null);
|
||||
Assert.True(vm.IsYouTubeActivity);
|
||||
Assert.True(vm.ShowYouTubeModeChoices);
|
||||
Assert.False(vm.ShowYouTubeStart);
|
||||
|
||||
vm.ChooseLiveModeCommand.Execute(null);
|
||||
Assert.True(vm.IsLiveModeChosen);
|
||||
Assert.False(vm.ShowYouTubeModeChoices);
|
||||
Assert.True(vm.ShowYouTubeStart);
|
||||
|
||||
vm.ChoosePrivateModeCommand.Execute(null);
|
||||
Assert.True(vm.IsPrivateModeChosen);
|
||||
Assert.False(vm.IsLiveModeChosen);
|
||||
|
||||
// Back to Build: editing returns, and the YouTube choice is forgotten, not carried.
|
||||
vm.SelectBuildActivityCommand.Execute(null);
|
||||
Assert.True(vm.IsBuildActivity);
|
||||
Assert.True(vm.IsEditMode);
|
||||
Assert.False(vm.ShowYouTubeModeChoices);
|
||||
Assert.False(vm.ShowYouTubeStart);
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
try { File.Delete(db); } catch (IOException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,22 +84,21 @@ public sealed class TopBarModeTests
|
||||
Assert.False(vm.ShowTestButton);
|
||||
Assert.Equal("Start Recording", vm.PrimaryStartButtonLabel);
|
||||
|
||||
// A local recording STAYS in the output world (build 730): the pill carries REC, the
|
||||
// clock and the stop, so the separate reality line stays away — it would be a second set
|
||||
// of the same facts.
|
||||
vm.EnterOutputWorldCommand.Execute(null);
|
||||
vm.RecordPillOn = true;
|
||||
vm.OnAirPillOn = false;
|
||||
// A local recording lives in the Record activity (2026-11-13): the bar keeps its three
|
||||
// buttons and the Record one carries REC + the clock + Stop, so the separate reality line
|
||||
// stays away — it would be a second set of the same facts.
|
||||
vm.SelectRecordActivityCommand.Execute(null);
|
||||
vm.IsRecording = true;
|
||||
Assert.True(vm.ShowOutputStage);
|
||||
Assert.True(vm.IsRecordActivity);
|
||||
Assert.True(vm.ShowRecordStop);
|
||||
Assert.False(vm.RunningVisible);
|
||||
Assert.StartsWith("REC: ", vm.RecordChipText);
|
||||
Assert.StartsWith("REC: ", vm.RecordElapsedLabel);
|
||||
vm.IsRecording = false;
|
||||
|
||||
// A live stream retires the stage instead; the reality line pins the mode.
|
||||
// A live stream is the YouTube activity's flow; the reality line pins the mode.
|
||||
vm.SelectYouTubeActivityCommand.Execute(null);
|
||||
vm.StreamStatus = StreamStatus.Streaming;
|
||||
Assert.True(vm.RunningVisible);
|
||||
Assert.False(vm.ShowOutputStage);
|
||||
Assert.True(vm.ShowYouTubeStop);
|
||||
Assert.Equal("LIVE", vm.RunningText);
|
||||
Assert.Equal("#ff5451", vm.RunningDotBrush);
|
||||
vm.IsTestStream = true;
|
||||
|
||||
@@ -29,10 +29,11 @@ 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("Tag=\"{Binding IsBuildWorld}\"", topBar);
|
||||
Assert.Contains("Tag=\"{Binding IsOutputWorld}\"", topBar);
|
||||
Assert.Contains("Text=\"Build Scenes\"", topBar);
|
||||
Assert.Contains("Text=\"YouTube\"", topBar);
|
||||
Assert.Contains("Text=\"Record\"", topBar);
|
||||
Assert.Contains("Tag=\"{Binding IsBuildActivity}\"", topBar);
|
||||
Assert.Contains("Tag=\"{Binding IsYouTubeActivity}\"", topBar);
|
||||
Assert.DoesNotContain("x:Name=\"EditBody\"", topBar);
|
||||
|
||||
// 2b. The segment is Build-world only: choosing Record / Go Live REPLACES it with the output
|
||||
@@ -42,18 +43,16 @@ public class UiShellTests
|
||||
Assert.Contains("Brush.SegmentTrack", topBar);
|
||||
Assert.Contains("Style=\"{StaticResource SegmentHalf}\"", topBar);
|
||||
Assert.Contains("Style=\"{StaticResource SegmentIcon}\"", topBar);
|
||||
Assert.Contains("Visibility=\"{Binding IsBuildWorld, Converter={StaticResource BoolToVis}}\"", topBar);
|
||||
Assert.Contains("Text=\"Output:\"", topBar);
|
||||
Assert.Contains("Text=\"<esc> 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.
|
||||
Assert.Contains("Text=\"Output:\"", topBar);
|
||||
|
||||
Assert.Contains("Text=\"YouTube\"", topBar);
|
||||
Assert.Contains("Text=\"Local MP4\"", topBar);
|
||||
Assert.Contains("Text=\"One output at a time\"", topBar);
|
||||
Assert.Contains("Command=\"{Binding SelectYouTubeCommand}\"", topBar);
|
||||
Assert.Contains("Command=\"{Binding SelectLocalMp4Command}\"", topBar);
|
||||
Assert.Contains("Command=\"{Binding SelectYouTubeActivityCommand}\"", topBar);
|
||||
Assert.Contains("Command=\"{Binding SelectRecordActivityCommand}\"", 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).
|
||||
@@ -120,9 +119,9 @@ public class UiShellTests
|
||||
|
||||
// The world segment leads the centre cluster; the brand column carries no edit control.
|
||||
var centre = topBar.IndexOf("<StackPanel Grid.Column=\"1\"", System.StringComparison.Ordinal);
|
||||
var segment = topBar.IndexOf("Text=\"Build scenes\"", System.StringComparison.Ordinal);
|
||||
var segment = topBar.IndexOf("Text=\"Build Scenes\"", System.StringComparison.Ordinal);
|
||||
Assert.True(centre > 0 && segment > centre,
|
||||
"the [ Build Scenes ][ Record / Go Live ] segment must lead the centre cluster");
|
||||
"the three-activity segment must lead the centre cluster");
|
||||
Assert.DoesNotContain("x:Name=\"EditBody\"", topBar);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user