Files
OpenNest/OpenNest.Desktop.Tests/NestTabsSaveTests.cs
T
ajandClaude Opus 5.5 8e87c0881a feat(desktop): save a nest tab to a file or export a copy
NestTabs.SaveAs writes the tab's nest, makes the file its file and name,
and clears its unsaved mark; a write that fails throws and changes
nothing. ExportCopy writes the nest without touching its file, name or
unsaved mark.

Both refuse, before writing anything, a file another open tab shows (so
one file never has two tabs) and a tab that has been closed; Export
also refuses the tab's own file, which only Save may write. Refusals
are a SaveTargetRefusedException whose message says what to do.

SavePath is the file Save writes without asking: only one saved to this
session, as in the WinForms app, so a nest that was only opened asks
for a file on its first Save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:32:13 -05:00

196 lines
6.1 KiB
C#

using OpenNest.IO;
namespace OpenNest.Desktop.Tests;
public sealed class NestTabsSaveTests : IDisposable
{
private readonly string directory =
Directory.CreateTempSubdirectory("opennest-desktop-tabs-save-").FullName;
public void Dispose() => Directory.Delete(directory, recursive: true);
private string InDir(string name) => Path.Combine(directory, name);
// Opens a real nest file in a new tab, as File > Open does.
private NestTab OpenSaved(NestTabs tabs, string name)
{
var path = InDir(name);
var nest = TestNests.OneSquare();
nest.Name = Path.GetFileNameWithoutExtension(name);
Assert.True(new NestWriter(nest).Write(path));
return tabs.Open(path, file => new Document { Nest = new NestReader(file).Read() });
}
[Fact]
public void SavePath_IsNullForANestOnlyOpened_SoTheFirstSaveAsks()
{
var tabs = new NestTabs();
var tab = OpenSaved(tabs, "opened.nest");
Assert.Null(tab.SavePath);
}
[Fact]
public void SaveAs_WritesTheFileAndMakesItTheTabsFileNameAndSavePath()
{
var tabs = new NestTabs();
var tab = OpenSaved(tabs, "opened.nest");
tab.MarkModified();
var changed = 0;
tabs.Changed += (_, _) => changed++;
var target = InDir("Renamed Job.nest");
tabs.SaveAs(tab, target);
Assert.Equal(target, tab.Path);
Assert.Equal(target, tab.SavePath);
Assert.Equal("Renamed Job", tab.Name);
Assert.False(tab.Modified);
Assert.Equal(1, changed);
var read = new NestReader(target).Read();
Assert.Equal("Renamed Job", read.Name);
Assert.Single(read.Plates[0].Parts);
}
[Fact]
public void SaveAs_ThenOpeningTheSavedFile_SwitchesToTheTabInsteadOfOpeningItTwice()
{
var tabs = new NestTabs();
var tab = OpenSaved(tabs, "opened.nest");
OpenSaved(tabs, "other.nest");
var target = InDir("saved.nest");
tabs.SaveAs(tab, target);
var again = tabs.Open(target, _ => throw new InvalidOperationException("loaded again"));
Assert.Same(tab, again);
Assert.Equal(2, tabs.Tabs.Count);
}
[Fact]
public void SaveAs_AFileAnotherTabShows_IsRefusedAndWritesNothing()
{
var tabs = new NestTabs();
var a = OpenSaved(tabs, "a.nest");
var b = OpenSaved(tabs, "b.nest");
a.MarkModified();
var before = File.ReadAllBytes(b.Path);
var ex = Assert.Throws<SaveTargetRefusedException>(() => tabs.SaveAs(a, b.Path));
Assert.Contains("b.nest is open in the b tab", ex.Message);
Assert.Equal(before, File.ReadAllBytes(b.Path));
Assert.Equal(InDir("a.nest"), a.Path);
Assert.Equal("a", a.Name);
Assert.True(a.Modified);
}
[Fact]
public void SaveAs_OntoItsOwnFile_IsAllowed()
{
var tabs = new NestTabs();
var tab = OpenSaved(tabs, "own.nest");
tab.MarkModified();
tabs.SaveAs(tab, tab.Path);
Assert.False(tab.Modified);
Assert.Equal(InDir("own.nest"), tab.SavePath);
}
[Fact]
public void SaveAs_AWriteThatFails_KeepsTheFileNameAndUnsavedMark()
{
var tabs = new NestTabs();
var tab = OpenSaved(tabs, "kept.nest");
tab.MarkModified();
var changed = 0;
tabs.Changed += (_, _) => changed++;
Assert.ThrowsAny<IOException>(
() => tabs.SaveAs(tab, InDir(Path.Combine("missing folder", "x.nest"))));
Assert.Equal(InDir("kept.nest"), tab.Path);
Assert.Equal("kept", tab.Name);
Assert.Null(tab.Document.LastSavePath);
Assert.True(tab.Modified);
Assert.Equal(0, changed);
}
[Fact]
public void ExportCopy_WritesTheNestButKeepsTheTabsFileNameAndUnsavedMark()
{
var tabs = new NestTabs();
var tab = OpenSaved(tabs, "job.nest");
tab.MarkModified();
var target = InDir("copy.nest");
tabs.ExportCopy(tab, target);
Assert.Equal(InDir("job.nest"), tab.Path);
Assert.Equal("job", tab.Name);
Assert.Null(tab.SavePath);
Assert.True(tab.Modified);
var read = new NestReader(target).Read();
Assert.Equal("job", read.Name);
Assert.Single(read.Plates[0].Parts);
}
[Fact]
public void SaveAs_AFailedWriteAfterAnEarlierSave_KeepsTheEarlierSavePath()
{
var tabs = new NestTabs();
var tab = OpenSaved(tabs, "first.nest");
var earlier = InDir("earlier.nest");
tabs.SaveAs(tab, earlier);
Assert.ThrowsAny<IOException>(
() => tabs.SaveAs(tab, InDir(Path.Combine("missing folder", "x.nest"))));
Assert.Equal(earlier, tab.SavePath);
Assert.Equal(earlier, tab.Path);
}
[Fact]
public void SaveAs_ATabThatWasClosed_IsRefusedAndWritesNothing()
{
var tabs = new NestTabs();
var tab = OpenSaved(tabs, "closed.nest");
tabs.Close(tab);
var target = InDir("after-close.nest");
Assert.Throws<SaveTargetRefusedException>(() => tabs.SaveAs(tab, target));
Assert.Throws<SaveTargetRefusedException>(() => tabs.ExportCopy(tab, target));
Assert.False(File.Exists(target));
}
[Fact]
public void ExportCopy_OntoTheTabsOwnFile_IsRefusedAndWritesNothing()
{
var tabs = new NestTabs();
var tab = OpenSaved(tabs, "own.nest");
tab.Document.Nest.Notes = "not saved";
var before = File.ReadAllBytes(tab.Path);
var ex = Assert.Throws<SaveTargetRefusedException>(() => tabs.ExportCopy(tab, tab.Path));
Assert.Contains("File > Save", ex.Message);
Assert.Equal(before, File.ReadAllBytes(tab.Path));
}
[Fact]
public void ExportCopy_ToAFileAnotherTabShows_IsRefusedAndWritesNothing()
{
var tabs = new NestTabs();
var a = OpenSaved(tabs, "a.nest");
var b = OpenSaved(tabs, "b.nest");
var before = File.ReadAllBytes(b.Path);
Assert.Throws<SaveTargetRefusedException>(() => tabs.ExportCopy(a, b.Path));
Assert.Equal(before, File.ReadAllBytes(b.Path));
}
}