Files
LlamaCasty/ytLive.Tests/RecordingSaveDialogTests.cs
T
gramps e3e69d941d recording save dialog: Cancel discards the footage instead of saving the default
The creator reported that clicking Cancel in the post-recording save prompt still
saved the file under the default name: "cancel should cancel the save option,
discarding the recording" — Enter is what accepts the default.

The modal was always correct (Enter -> DialogResult=true, Cancel/Escape/X -> false).
The bug was in the caller: FinalizeRecordingAsync only overwrote `stem` when the
dialog returned true and then ran File.Move UNCONDITIONALLY, so a falsy result fell
straight through into the save. The nullable stem made "I don't want this" look
like "I accept the default".

Extracted the decision into internal MainViewModel.CompleteRecordingSave so it is
testable against real files without a frame pump:
  creatorSaved == false (Cancel/Escape/X) -> delete the temp file, leave the
      videos folder empty; a failed delete returns DiscardFailed and the toast
      names the file + folder (a "discarded" recording still on disk is worse
      than no report).
  creatorSaved == true (Save, or Enter on the pre-filled default) -> File.Move.
      A blank box still means "keep the auto name", but only on an explicit Save.

Test: ytLive.Tests/RecordingSaveDialogTests.cs — 5 facts over real temp files. The
headline asserts Cancel leaves the directory EMPTY, not merely "the stem is
unchanged"; a modal's own test cannot cover the decision it feeds.

Full suite 362/362 (the RealMouseDrag flake passed this run).
2026-09-27 09:25:21 -07:00

118 lines
4.8 KiB
C#

using System;
using System.IO;
using Xunit;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>
/// The post-recording save dialog (CREATOR RULING 2026-09-26).
/// <para>Reported symptom: clicking Cancel in the save prompt still saved the
/// recording under the default name — Cancel behaved like "accept the default". The
/// creator's intent is explicit: <b>Enter accepts the default file name and saves;
/// Cancel cancels the save and discards the footage.</b></para>
/// <para>The bug was in the caller, not the modal — <c>FinalizeRecordingAsync</c> ran
/// <c>File.Move</c> unconditionally, so <c>ShowDialog() == false</c> fell straight
/// through into the save. These tests drive the extracted commit-or-discard decision
/// against real files in a temp folder, which is what would have caught it.</para>
/// </summary>
public sealed class RecordingSaveDialogTests : IDisposable
{
private readonly string _dir = Path.Combine(
Path.GetTempPath(), "ytLive-recsave-" + Guid.NewGuid().ToString("N")[..8]);
private const string AutoStem = "ty-20260926-1405-0130";
private const string StartStem = "ty-20260926-1405-0000";
private const string Bytes = "fake-mp4-payload";
public RecordingSaveDialogTests() => Directory.CreateDirectory(_dir);
public void Dispose()
{
try { Directory.Delete(_dir, recursive: true); } catch { /* best-effort */ }
}
/// <summary>The reported bug: Cancel must leave NOTHING in the videos folder — not
/// the temp start-name file, and not a renamed copy of it either.</summary>
[Fact]
public void Cancel_DiscardsTheFootage_AndLeavesNothingOnDisk()
{
var start = WriteStartFile();
var result = CompleteRecordingSave(start, _dir, AutoStem, chosenStem: null, creatorSaved: false);
Assert.Equal(MainViewModel.RecordingSaveOutcome.Discarded, result.Outcome);
Assert.False(File.Exists(start), "Cancel must delete the temp recording, not move it");
Assert.Empty(Directory.GetFiles(_dir));
}
/// <summary>Enter on the pre-filled default is a Save — the file lands under the
/// auto name with its bytes intact (the creator's other half of the ruling).</summary>
[Fact]
public void EnterOnTheDefault_KeepsTheAutoName_AndSavesTheFootage()
{
var start = WriteStartFile();
var result = CompleteRecordingSave(start, _dir, AutoStem, AutoStem, creatorSaved: true);
Assert.Equal(MainViewModel.RecordingSaveOutcome.Saved, result.Outcome);
var expected = Path.Combine(_dir, AutoStem + ".mp4");
Assert.Equal(expected, result.FinalPath);
Assert.False(File.Exists(start), "Save renames the temp file — it must not be left behind");
Assert.Equal(Bytes, File.ReadAllText(expected));
Assert.Single(Directory.GetFiles(_dir));
}
/// <summary>A cleared name box on an explicit Save still means "keep the auto
/// name" (the dialog promises this) — Cancel is the only way to discard.</summary>
[Fact]
public void SaveWithABlankName_StillFallsBackToTheAutoName()
{
var start = WriteStartFile();
var result = CompleteRecordingSave(start, _dir, AutoStem, " ", creatorSaved: true);
Assert.Equal(MainViewModel.RecordingSaveOutcome.Saved, result.Outcome);
Assert.True(File.Exists(Path.Combine(_dir, AutoStem + ".mp4")));
}
/// <summary>An explicit Save with a chosen name honours that name.</summary>
[Fact]
public void SaveWithAChosenName_UsesThatName()
{
var start = WriteStartFile();
var result = CompleteRecordingSave(start, _dir, AutoStem, "my-stream", creatorSaved: true);
Assert.Equal("my-stream.mp4", Path.GetFileName(result.FinalPath!));
}
/// <summary>Never clobber an earlier recording: a second save with the same name
/// gets uniquified rather than overwriting the first.</summary>
[Fact]
public void SaveWithACollidingName_UniquifiesInsteadOfOverwriting()
{
var first = WriteStartFile();
Assert.Equal(
MainViewModel.RecordingSaveOutcome.Saved,
CompleteRecordingSave(first, _dir, AutoStem, AutoStem, true).Outcome);
var second = WriteStartFile();
var result = CompleteRecordingSave(second, _dir, AutoStem, AutoStem, true);
Assert.Equal(AutoStem + "-2.mp4", Path.GetFileName(result.FinalPath!));
Assert.Equal(2, Directory.GetFiles(_dir).Length);
}
private static MainViewModel.RecordingSaveResult CompleteRecordingSave(
string startPath, string dir, string autoStem, string? chosenStem, bool creatorSaved)
=> MainViewModel.CompleteRecordingSave(startPath, dir, autoStem, chosenStem, creatorSaved);
private string WriteStartFile()
{
var start = Path.Combine(_dir, StartStem + ".mp4");
File.WriteAllText(start, Bytes);
return start;
}
}