feat(topbar): three fixed slots — every state changes inside its own slot
Creator correction (2026-11-13): "OpenCode literally inserted Live and Private as two extra flat siblings
inside the outer pill bar, shoving Record to the right and making it look like five ungrouped toolbar
items". Correct — completion states were being added to the BAR instead of to a SLOT.
Now the bar is exactly three slots, and the bar's shape never changes:
[ Build Scenes ] [ YouTube ] [ Record ]
YouTube: ▶ YouTube -> [Live | Private | ✕] -> [● Start Live | ✕] (amber: next click goes on air)
-> [■ Stop Live]
Record: ◉ Record -> [● Start Recording | ✕] -> ● REC hh:mm:ss [■ Stop]
Build: enabled only while BOTH action slots are Idle; otherwise disabled at 40% with the tooltip
"Scene editing is locked while streaming or recording"
State lives in YouTubeSlotState / RecordSlotState, with one RaiseSlotStates() announcing every flag so no
caller can forget one. Esc and the ✕ cancel the slot you are in and nothing else.
This retires the BarWorld model from earlier today, along with TopBarActivityTests (superseded by
TopBarSlotTests, which pins all three flows, the lock, and cancel-to-Idle).
679 pass, 0 failed (the host-hang fallback in f4caea0 counted the per-test lines).
Scope justification: TopBarActivityTests.cs is deleted as part of this unit — it tested the model being
replaced; TopBarSlotTests is its successor.
This commit is contained in:
@@ -1,80 +0,0 @@
|
||||
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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,18 +87,16 @@ public sealed class TopBarModeTests
|
||||
// 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.IsRecordActivity);
|
||||
Assert.True(vm.ShowRecordStop);
|
||||
vm.OpenRecordCommand.Execute(null);
|
||||
vm.StartRecordingCommand.Execute(null);
|
||||
Assert.True(vm.IsRecordRecording);
|
||||
Assert.False(vm.RunningVisible);
|
||||
Assert.StartsWith("REC: ", vm.RecordElapsedLabel);
|
||||
vm.IsRecording = false;
|
||||
vm.StopRecordingCommand.Execute(null);
|
||||
|
||||
// A live stream is the YouTube activity's flow; the reality line pins the mode.
|
||||
vm.SelectYouTubeActivityCommand.Execute(null);
|
||||
// A live stream is the YouTube slot's flow; the reality line pins the mode.
|
||||
vm.StreamStatus = StreamStatus.Streaming;
|
||||
Assert.True(vm.ShowYouTubeStop);
|
||||
Assert.True(vm.RunningVisible);
|
||||
Assert.Equal("LIVE", vm.RunningText);
|
||||
Assert.Equal("#ff5451", vm.RunningDotBrush);
|
||||
vm.IsTestStream = true;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The bar is three FIXED SLOTS (creator, 2026-11-13). Each slot owns its own flow and mutates only its
|
||||
/// own interior, so neighbouring buttons never move; and while either action slot is past Idle, Build
|
||||
/// Scenes is locked.
|
||||
/// </summary>
|
||||
[Collection("MainViewModelLayoutPath")]
|
||||
public class TopBarSlotTests
|
||||
{
|
||||
[Fact]
|
||||
public void Each_slot_runs_its_own_flow_in_place_and_build_locks_while_they_do()
|
||||
{
|
||||
var db = Path.Combine(Path.GetTempPath(), $"llamacasty-slots-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = db;
|
||||
try
|
||||
{
|
||||
var vm = new MainViewModel();
|
||||
|
||||
// All three idle: YouTube and Record are single buttons, Build is live.
|
||||
Assert.True(vm.IsYouTubeIdle);
|
||||
Assert.True(vm.IsRecordIdle);
|
||||
Assert.True(vm.IsBuildScenesEnabled);
|
||||
Assert.Contains("Edit layers", vm.BuildScenesTooltip);
|
||||
|
||||
// YouTube: idle -> choose privacy -> armed -> (start) streaming.
|
||||
vm.OpenYouTubeCommand.Execute(null);
|
||||
Assert.True(vm.IsYouTubeChoosingPrivacy);
|
||||
Assert.False(vm.IsBuildScenesEnabled);
|
||||
Assert.Contains("locked", vm.BuildScenesTooltip);
|
||||
|
||||
vm.ChoosePrivateCommand.Execute(null);
|
||||
Assert.True(vm.IsYouTubeArmed);
|
||||
Assert.Equal("Start Private", vm.YouTubeArmLabel);
|
||||
|
||||
vm.ChooseLiveCommand.Execute(null); // switching choice while armed is allowed
|
||||
Assert.Equal("Start Live", vm.YouTubeArmLabel);
|
||||
|
||||
// Cancel returns that slot to Idle — and Build unlocks because both slots are idle again.
|
||||
vm.CancelYouTubeCommand.Execute(null);
|
||||
Assert.True(vm.IsYouTubeIdle);
|
||||
Assert.True(vm.IsBuildScenesEnabled);
|
||||
|
||||
// Record: idle -> armed -> (start) recording, with the clock carrying REC.
|
||||
vm.OpenRecordCommand.Execute(null);
|
||||
Assert.True(vm.IsRecordArmed);
|
||||
Assert.False(vm.IsBuildScenesEnabled);
|
||||
|
||||
vm.StartRecordingCommand.Execute(null);
|
||||
Assert.True(vm.IsRecordRecording);
|
||||
Assert.StartsWith("REC: ", vm.RecordElapsedLabel);
|
||||
|
||||
vm.StopRecordingCommand.Execute(null);
|
||||
Assert.True(vm.IsRecordIdle);
|
||||
Assert.True(vm.IsBuildScenesEnabled);
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
try { File.Delete(db); } catch (IOException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,8 @@ public class UiShellTests
|
||||
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.Contains("IsEnabled=\"{Binding IsBuildScenesEnabled}\"", topBar);
|
||||
Assert.Contains("Command=\"{Binding OpenYouTubeCommand}\"", topBar);
|
||||
Assert.DoesNotContain("x:Name=\"EditBody\"", topBar);
|
||||
|
||||
// 2b. The segment is Build-world only: choosing Record / Go Live REPLACES it with the output
|
||||
@@ -51,8 +51,8 @@ public class UiShellTests
|
||||
// 2c. The output stage: two labelled switches and the exclusivity promise beside them.
|
||||
|
||||
Assert.Contains("Text=\"YouTube\"", topBar);
|
||||
Assert.Contains("Command=\"{Binding SelectYouTubeActivityCommand}\"", topBar);
|
||||
Assert.Contains("Command=\"{Binding SelectRecordActivityCommand}\"", topBar);
|
||||
Assert.Contains("Command=\"{Binding OpenYouTubeCommand}\"", topBar);
|
||||
Assert.Contains("Command=\"{Binding OpenRecordCommand}\"", 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).
|
||||
|
||||
Reference in New Issue
Block a user