diff --git a/Tools/OutWit.Database.Studio.Tests/Themes/DesignTokenTests.cs b/Tools/OutWit.Database.Studio.Tests/Themes/DesignTokenTests.cs
index bea7805..f514e80 100644
--- a/Tools/OutWit.Database.Studio.Tests/Themes/DesignTokenTests.cs
+++ b/Tools/OutWit.Database.Studio.Tests/Themes/DesignTokenTests.cs
@@ -513,10 +513,13 @@ public void TheCensusOfHardCodedValuesIsWhatItWasMeasuredToBeTest()
// to open at a fixed 480 for content needing about 900 and could not be resized, so
// the footer was painted over. A window's minimum size is a window size, which is
// the remainder this ledger exists to allow.
- Assert.That(height, Is.EqualTo(68), "heights written by hand");
+ // 69, and the newest is the import preview's MaxHeight: the wizard shows the rows it
+ // parsed, and without a bound it would grow with the file. A box round a block of
+ // content is the other thing this ledger allows, beside window sizes.
+ Assert.That(height, Is.EqualTo(69), "heights written by hand");
Assert.That(HeightSiteKinds(), Is.EqualTo(new[]
{
- "Border:23", "Button:1", "DataGrid:2", "Ellipse:1", "Grid:1", "GridSplitter:1",
+ "Border:24", "Button:1", "DataGrid:2", "Ellipse:1", "Grid:1", "GridSplitter:1",
"ListBox:1", "PathIcon:2", "ProgressBar:4", "ScrollViewer:3", "TextBox:1",
"Window:28",
}));
diff --git a/Tools/OutWit.Database.Studio.Tests/ViewModels/TheExportDialogSaysWhichExportItIsTests.cs b/Tools/OutWit.Database.Studio.Tests/ViewModels/TheExportDialogSaysWhichExportItIsTests.cs
new file mode 100644
index 0000000..3cd9e72
--- /dev/null
+++ b/Tools/OutWit.Database.Studio.Tests/ViewModels/TheExportDialogSaysWhichExportItIsTests.cs
@@ -0,0 +1,165 @@
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using OutWit.Database.Studio.Tests.Helpers;
+using OutWit.Database.Studio.ViewModels;
+
+namespace OutWit.Database.Studio.Tests.ViewModels;
+
+///
+/// Two exports, two names - and a progress panel that waits until it has a number.
+///
+///
+///
+/// Finding 5:Tools ▸ Export… and Export Results… are not the same dialog - the
+/// first exports a TABLE chosen from a list, the second exports the query result and carries the three
+/// scopes with their row counts - and both were called Export Data in the title bar, under a
+/// menu entry called Export…. Somebody looking in Tools for the scopes will not find them.
+///
+///
+/// Finding 32: the first tick of a large export read Exporting... 0 / 0 rows at
+/// 0.0 %, because the panel opens when the export starts and the rows are fetched after that.
+/// Zero out of zero is not a small number, it is no number - and it is the frame anybody
+/// photographing an export in progress is most likely to catch.
+///
+///
+/// Finding 4: the output path could only be set with Browse…, which opens the system
+/// dialog on the user's own folders - awkward while a screen is being recorded, and slower than
+/// typing a path somebody already knows.
+///
+///
+[TestFixture]
+public class TheExportDialogSaysWhichExportItIsTests
+{
+ #region Finding 5
+
+ [TestCase("en")]
+ [TestCase("ru")]
+ public void TheTwoExportsHaveTwoNamesTest(string language)
+ {
+ using var catalogue = JsonDocument.Parse(Source($"Resources/Strings.{language}.json"));
+
+ var table = catalogue.RootElement.GetProperty("Dialog.Export.Title.Table").GetString();
+ var results = catalogue.RootElement.GetProperty("Dialog.Export.Title.Results").GetString();
+ var menu = catalogue.RootElement.GetProperty("Menu.Export").GetString();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(table, Is.Not.EqualTo(results),
+ "the two windows do not share a name, because they do not do the same thing");
+
+ Assert.That(menu, Is.Not.Null.And.Not.Empty);
+
+ Assert.That(menu!.Replace("_", string.Empty).TrimEnd('.', '…'),
+ Does.Contain(table!.Split(' ')[^1]),
+ "and the menu entry says which of the two it opens");
+ });
+ }
+
+ [Test]
+ public async Task TheWindowNamesItselfAfterWhatItIsExportingTest()
+ {
+ await using var studio = await StudioFixture.CreateAsync();
+
+ using var export = new ExportViewModel(studio.App);
+
+ var forTable = export.WindowTitle;
+
+ // The same window, handed a query result: this is the call the result grid makes.
+ using var data = new System.Data.DataTable();
+ data.Columns.Add("Id");
+
+ export.SetDataSource(data, "SELECT 1", null, 0);
+
+ Assert.That(export.WindowTitle, Is.Not.EqualTo(forTable),
+ "the same window used for the other job says so in its title");
+ }
+
+ #endregion
+
+ #region Finding 32
+
+ [Test]
+ public async Task NoNumberIsShownBeforeThereIsOneTest()
+ {
+ await using var studio = await StudioFixture.CreateAsync();
+
+ using var export = new ExportViewModel(studio.App);
+
+ Assert.That(export.KnowsHowManyRows, Is.False,
+ "nothing has been counted yet, so there is nothing to say");
+
+ var markup = Source("Views/Dialogs/ExportDialog.axaml");
+
+ var progress = Regex.Match(markup, @"]*ExportProgressText[\s\S]*?/>");
+
+ Assert.That(progress.Success, Is.True);
+
+ Assert.That(progress.Value, Does.Contain("KnowsHowManyRows"),
+ "and the panel's numbers wait for the total rather than opening at 0 / 0");
+ }
+
+ #endregion
+
+ #region Finding 4
+
+ [Test]
+ public void TheOutputPathCanBeTypedTest()
+ {
+ var markup = Source("Views/Dialogs/ExportDialog.axaml");
+
+ var box = Regex.Match(markup, @"]*Text=""\{Binding OutputPath\}""[\s\S]*?/>");
+
+ Assert.That(box.Success, Is.True, "the dialog has a box for the path");
+
+ Assert.That(box.Value, Does.Not.Contain("IsReadOnly=\"True\""),
+ "a path can be typed as well as browsed for");
+ }
+
+ #endregion
+
+ #region Finding 28
+
+ [Test]
+ public void TheCollisionPolicyIsOnTheStepAboutTheTargetTest()
+ {
+ var markup = Source("Views/Dialogs/ImportDialog.axaml");
+
+ var block = Regex.Match(markup,
+ @"]*Grid\.Row=""3""[\s\S]*?ImportConflictSkip");
+
+ Assert.That(block.Success, Is.True, "the collision policy is in the wizard");
+
+ Assert.That(block.Value, Does.Contain("IsDestination"),
+ "and on the step named after the thing it is about - it sat on «Columns», which left that "
+ + "step holding two unrelated decisions");
+ }
+
+ #endregion
+
+ #region Tools
+
+ private static string Source(string relative)
+ {
+ var directory = new DirectoryInfo(AppContext.BaseDirectory);
+
+ while (directory != null)
+ {
+ var candidate = Path.Combine(directory.FullName, "Tools", "OutWit.Database.Studio");
+
+ if (Directory.Exists(Path.Combine(candidate, "Views")))
+ {
+ var path = Path.Combine(candidate, relative.Replace('/', Path.DirectorySeparatorChar));
+
+ Assert.That(File.Exists(path), Is.True, $"{relative} must be where this fixture says");
+
+ return File.ReadAllText(path);
+ }
+
+ directory = directory.Parent;
+ }
+
+ throw new AssertionException("the Studio project was not found from " + AppContext.BaseDirectory);
+ }
+
+ #endregion
+}
diff --git a/Tools/OutWit.Database.Studio.Tests/ViewModels/TheImportWizardSaysWhatItSawTests.cs b/Tools/OutWit.Database.Studio.Tests/ViewModels/TheImportWizardSaysWhatItSawTests.cs
new file mode 100644
index 0000000..71ac9d1
--- /dev/null
+++ b/Tools/OutWit.Database.Studio.Tests/ViewModels/TheImportWizardSaysWhatItSawTests.cs
@@ -0,0 +1,199 @@
+using System.Text.RegularExpressions;
+using OutWit.Database.Studio.Tests.Helpers;
+using OutWit.Database.Studio.ViewModels;
+
+namespace OutWit.Database.Studio.Tests.ViewModels;
+
+///
+/// The import wizard reports the file it read, and keeps every row the database refused.
+///
+///
+///
+/// Finding 25: step 1 said File contains approximately 13 rows 0 columns for a CSV
+/// with four, and Refresh Preview changed nothing. The count was read from
+/// ColumnMappings, which is built on step 3 - so on step 1 it was asking a list that does not
+/// exist yet. Catching a wrong delimiter before anything is written is what a preview is for, and
+/// "0 columns" is exactly what a wrong delimiter would look like if the number meant anything.
+///
+///
+/// Finding 27 was half built, which is why it was reported as absent. The CSV path has kept
+/// every rejection since WS-36 and the window has a button that writes them to a file beside the
+/// source. The JSON path threw away everything past the tenth - it filled the display list and
+/// recorded nothing - so an import of a JSON file really did report "15 failed", name ten, and have
+/// nothing to save. Both paths record now.
+///
+///
+/// Finding 26: the result was painted across the line it replaces because two borders share
+/// one grid cell. A cell holds one thing.
+///
+///
+[TestFixture]
+public class TheImportWizardSaysWhatItSawTests
+{
+ #region Fields
+
+ private StudioFixture m_studio = null!;
+ private ImportViewModel m_import = null!;
+ private string m_csv = null!;
+
+ #endregion
+
+ #region Setup
+
+ [SetUp]
+ public async Task SetUp()
+ {
+ m_studio = await StudioFixture.CreateAsync();
+
+ m_csv = Path.Combine(m_studio.Root, "people.csv");
+
+ await File.WriteAllTextAsync(m_csv,
+ "Id,Name,Email,City" + Environment.NewLine
+ + "1,Ada,ada@example.com,London" + Environment.NewLine
+ + "2,Grace,grace@example.com,New York" + Environment.NewLine);
+
+ m_import = new ImportViewModel(m_studio.App);
+
+ await m_import.InitializeAsync();
+ }
+
+ [TearDown]
+ public async Task TearDown()
+ {
+ m_import.Dispose();
+
+ await m_studio.DisposeAsync();
+ }
+
+ #endregion
+
+ #region Finding 25
+
+ [Test]
+ public async Task TheFirstStepCountsTheColumnsItReadTest()
+ {
+ m_import.InputPath = m_csv;
+
+ await StudioFixture.PressAsync(m_import.PreviewCommand);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(m_import.PreviewData, Is.Not.Null, "the file was parsed");
+
+ Assert.That(m_import.PreviewData!.Columns, Has.Count.EqualTo(4));
+
+ Assert.That(m_import.PreviewColumnsSummary, Does.Contain("4"),
+ "the line says four - it used to ask the mapping list, which is built two steps later");
+ });
+ }
+
+ [Test]
+ public async Task TheRowsItReadCanBeLookedAtTest()
+ {
+ m_import.InputPath = m_csv;
+
+ await StudioFixture.PressAsync(m_import.PreviewCommand);
+
+ Assert.That(m_import.PreviewData!.Rows, Has.Count.EqualTo(2),
+ "the parsed rows are there to be shown - a preview that shows nothing catches no delimiter");
+
+ Assert.That(Markup("Views/Dialogs/ImportDialog.axaml"), Does.Contain("ImportPreviewRowsGrid"),
+ "and the window draws them");
+ }
+
+ #endregion
+
+ #region Finding 26
+
+ [Test]
+ public void TheResultDoesNotLandOnTopOfThePreviewTest()
+ {
+ // Comments stripped first: this fixture is about the markup, and the comment explaining
+ // the fix mentions the cell it is about.
+ var markup = Regex.Replace(Markup("Views/Dialogs/ImportDialog.axaml"),
+ @"", string.Empty);
+
+ Assert.That(Regex.Matches(markup, @"Grid\.Row=""5"""), Has.Count.EqualTo(1),
+ "one thing in the cell: two borders in the same one are drawn over each other, which is "
+ + "how the result came to be painted across the line it replaces");
+ }
+
+ #endregion
+
+ #region Finding 27
+
+ ///
+ /// Both import paths keep every rejection, so the button that writes them out has them all.
+ ///
+ ///
+ /// Asserted on the source, because reaching the JSON path here would mean importing into a table
+ /// whose rows the engine refuses one at a time - a scenario that measures the engine rather than
+ /// this wizard. What is checked is that neither path records ONLY the ten it shows.
+ ///
+ [Test]
+ public void NeitherImportPathThrowsAwayARejectionTest()
+ {
+ var source = Source("ViewModels/ImportViewModel.cs");
+
+ var displayed = Regex.Matches(source, @"ImportErrors\.Add\(");
+ var recorded = Regex.Matches(source, @"Rejected\.Add\(");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(displayed, Has.Count.EqualTo(2), "CSV and JSON, one display list each");
+
+ Assert.That(recorded, Has.Count.EqualTo(2),
+ "and each of them records the rejection as well - the JSON path used to fill the "
+ + "display list and keep nothing");
+ });
+ }
+
+ [Test]
+ public void TheButtonThatWritesThemOutFollowsTheRejectionsTest()
+ {
+ var markup = Markup("Views/Dialogs/ImportDialog.axaml");
+
+ var button = Regex.Match(markup, @"
-
+
+ IsVisible="{Binding Step, Converter={x:Static vm:ImportSteps.IsDestination}}">
+
+
-
+
+
+
+
+
-
+