From 9988de7889313d32ac0ffbb93351601bead033e8 Mon Sep 17 00:00:00 2001 From: TheSevenPens <98374109+TheSevenPens@users.noreply.github.com> Date: Thu, 1 Oct 2026 05:48:28 -0700 Subject: [PATCH 1/2] Fix Format-VisioPage -Width/-Height and New-VisioHyperlink's boolean flags Format-VisioPage -Width and -Height always threw a COMException: PageCommands.SetFormatCells committed the page format cells to the Page object instead of its page sheet (SetPageSize already used the page sheet). New-VisioHyperlink accepted -NewWindow, -Default, -Invisible and -SortKey and ignored them: HyperlinkHelper.Add left those cells commented out, so they always stayed FALSE or unset. It now writes them to the shape sheet (the sort key quoted so it is a valid formula). Found by the second slice of cmdlet-binding tests (#173). Each fix has tests that fail without it. Co-Authored-By: Claude Sonnet 5.5 --- NuGet/CHANGELOG.md | 2 ++ .../VisioAutomation/Shapes/HyperlinkHelper.cs | 21 +++++++++++++++---- .../VisioPowerShell/CHANGELOG.md | 6 +++++- .../VisioScripting/Commands/PageCommands.cs | 3 ++- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/NuGet/CHANGELOG.md b/NuGet/CHANGELOG.md index 3c9ddae5b..c8ed63570 100644 --- a/NuGet/CHANGELOG.md +++ b/NuGet/CHANGELOG.md @@ -19,6 +19,8 @@ The format follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/ - On ``: a per-edge `connectortype` that overrides the page setting, a `` child (explicit cells win over the `color`, `weight` and arrow defaults), and `` children. ### Fixed +- `Client.Page.SetFormatCells` now writes the page format cells to the page sheet. Previously it committed them to the `Page` object, which always threw a `COMException`, so `Format-VisioPage -Width` / `-Height` could not work. Found by the cmdlet-binding tests ([#173](https://github.com/saveenr/VisioAutomation/issues/173)). +- `HyperlinkHelper.Add` (and so `Client.Hyperlink.AddHyperlink`) now applies the `NewWindow`, `Default`, `Invisible` and `SortKey` cells of the `HyperlinkCells` it is given. Previously it ignored them, so the cells always stayed `FALSE` (or unset). - `Client.Model.DrawOrgChart`, and so `Out-VisioApplication -OrgChart`, no longer resizes the page passed as its `TargetPage`. The chart is drawn in a new document, and each of its pages is already sized by the render, but the call also resized the caller's own page to fit its contents, which shrank that page for no reason. The `TargetPage` now only supplies the application. Closes [#219](https://github.com/saveenr/VisioAutomation/issues/219). - Custom properties set on a directed graph connector (`Edge.CustomProperties`) are now applied to the drawn connector, for both dynamic and Bezier connectors. Previously the renderer ignored them. - `Client.Model.DrawDirectedGraphDocument` now honors its `DirectedGraphStyling` parameter. Previously the argument was accepted and ignored, so every page was drawn with the default styling (the `Dynamic Connector` master from `connec_u.vss`). A null styling still means the default. Closes [#197](https://github.com/saveenr/VisioAutomation/issues/197). diff --git a/VisioAutomation_2010/VisioAutomation/Shapes/HyperlinkHelper.cs b/VisioAutomation_2010/VisioAutomation/Shapes/HyperlinkHelper.cs index b1cbe3e19..29866334c 100644 --- a/VisioAutomation_2010/VisioAutomation/Shapes/HyperlinkHelper.cs +++ b/VisioAutomation_2010/VisioAutomation/Shapes/HyperlinkHelper.cs @@ -41,11 +41,24 @@ public static int Add( hlinks_object.SubAddress = hyperlink.SubAddress.Value; hlinks_object.ExtraInfo = hyperlink.ExtraInfo.Value; - //hlinks_object.NewWindow = hyperlink.NewWindow.Formula.Value; - //hlinks_object.IsDefaultLink = hyperlink.Default.Formula.Value; - // hlinks_object.XXX = hyperlink.Invisible.Formula.Value; + // The Hyperlink object has no property for these cells, so write them to the shape sheet. + // Cells that were not set are skipped by the writer. + var other_cells = new HyperlinkCells(); + if (hyperlink.SortKey.HasValue) + { + // the sort key is a string cell, so it needs quoting to be a valid formula + other_cells.SortKey = Core.CellValue.EncodeValue(hyperlink.SortKey.Value); + } + other_cells.NewWindow = hyperlink.NewWindow; + other_cells.Default = hyperlink.Default; + other_cells.Invisible = hyperlink.Invisible; + + short row = hlinks_object.Row; + var writer = new ShapeSheet.Writers.SrcWriter(); + writer.SetValues(other_cells, row); + writer.Commit(shape, Core.CellValueType.Formula); - return hlinks_object.Row; + return row; } public static int Set( diff --git a/VisioAutomation_2010/VisioPowerShell/CHANGELOG.md b/VisioAutomation_2010/VisioPowerShell/CHANGELOG.md index f3820936b..6cc213039 100644 --- a/VisioAutomation_2010/VisioPowerShell/CHANGELOG.md +++ b/VisioAutomation_2010/VisioPowerShell/CHANGELOG.md @@ -12,7 +12,11 @@ The format follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/ ## [Unreleased] -_No consumer-visible changes yet._ +### Fixed +- `Format-VisioPage -Width` and `-Height` now work. Previously the cmdlet always threw a `COMException` when either was given, because the page format cells were written to the page object instead of its page sheet. (`-Orientation`, `-FitContents` and the other parameters were not affected.) +- `New-VisioHyperlink -NewWindow`, `-Default`, `-Invisible` and `-SortKey` now take effect. Previously the cmdlet accepted them and ignored them, so those hyperlink cells always stayed `FALSE` (or unset). + +Both were found by the second slice of cmdlet-binding tests ([#173](https://github.com/saveenr/VisioAutomation/issues/173)). ## [4.7.3] - 2026-09-30 diff --git a/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs b/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs index 48d3f00b2..e20673aaa 100644 --- a/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs +++ b/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs @@ -269,10 +269,11 @@ public void SetFormatCells(TargetPages targetpages, VisioAutomation.Pages.PageFo { foreach (var page in targetpages.Pages) { + // the page format cells live in the page sheet, so commit to it (committing the Page itself throws a COMException) var writer = new VisioAutomation.ShapeSheet.Writers.SrcWriter(); writer.SetValues(cells); writer.BlastGuards = true; - writer.Commit(page, VisioAutomation.Core.CellValueType.Formula); + writer.Commit(page.PageSheet, VisioAutomation.Core.CellValueType.Formula); } } } From 59c66c933fe363faa5ec6f32a473e2a46feb552e Mon Sep 17 00:00:00 2001 From: TheSevenPens <98374109+TheSevenPens@users.noreply.github.com> Date: Thu, 1 Oct 2026 05:48:28 -0700 Subject: [PATCH 2/2] Cmdlet-binding tests, second slice (#173) 46 new tests in VTest.PowerShell covering parameter-set resolution, mandatory parameters, switches, enums and pipeline binding for Get-VisioPage, Get-VisioShape, Get-VisioDocument, Select-VisioShape, New-VisioPage, Format-VisioPage, Remove-VisioPage, Format-VisioShape, New-VisioHyperlink, Set-VisioCustomProperty, Connect-VisioShape, Format-VisioWindow and the type-driven parameter sets of Out-VisioApplication. Adds CmdletScriptExtensions (RunInNewDocument, ExpectFailureInNewDocument, MessageOf) so each test runs one script in the runspace and returns plain values. Updates the test counts, the TESTING.md harness notes, the coverage gap list and CLAUDE.md. Get-VisioPage -ID's index-vs-ID question is filed as #232 and deliberately not asserted. Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 4 +- .../CmdletScriptExtensions.cs | 53 ++++ .../DocumentWindowCmdletBindingTests.cs | 120 +++++++++ .../PageCmdletBindingTests.cs | 159 ++++++++++++ .../ShapeCmdletBindingTests.cs | 245 ++++++++++++++++++ docs/TESTING.md | 6 +- docs/futures/test-coverage-gaps.md | 2 + 7 files changed, 586 insertions(+), 3 deletions(-) create mode 100644 VisioAutomation_2010/VTest.PowerShell/CmdletScriptExtensions.cs create mode 100644 VisioAutomation_2010/VTest.PowerShell/DocumentWindowCmdletBindingTests.cs create mode 100644 VisioAutomation_2010/VTest.PowerShell/PageCmdletBindingTests.cs create mode 100644 VisioAutomation_2010/VTest.PowerShell/ShapeCmdletBindingTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index 097ef181a..c6ae1b6dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,7 @@ The full suite exercises real Visio COM calls with no mock/fake layer. CI runs o - **Releases:** NuGet `VisioAutomation2010` **3.1.0** (tag `VisioAutomation_3.1.0`) and Visio PowerShell module **4.7.3** (tag `VisioPS_4.7.3`) shipped on 2026-09-30. The Release-build change for the NuGet flow ([#181](https://github.com/saveenr/VisioAutomation/issues/181)) is done and closed. - **Repo:** `master` at `92dec356` (PRs up to #227 merged), no open PRs, working trees clean. The older `claude/*` and `experiment/linq-shapesheet` branches are leftovers from earlier sessions. - **Unreleased changes** (`NuGet/CHANGELOG.md` `[Unreleased]`): the first tier of [#225](https://github.com/saveenr/VisioAutomation/issues/225) is Added (more directed graph XML options: ``, shape `width`/`height`, ``, ``, typed ``, and per-connector `connectortype`, `` and ``); the rest is Fixed: `DrawDataTable` honors widths and heights, `DrawDataTableModel` draws on the target page, `DrawXmlModel` roots the tree at the document element and now has an undo scope, `DrawDirectedGraphDocument` honors its `DirectedGraphStyling`, and `DrawOrgChart` no longer resizes the page you pass it. The next version is not decided; these are fixes only, so a patch release is the likely fit. -- **Tests:** 282 tests across four projects (VTest 108, VTest.Models 104, VTest.Scripting 43, VTest.PowerShell 27). The last full Release run (256) was on 2026-09-30; the newest tests are the regression tests for [#197](https://github.com/saveenr/VisioAutomation/issues/197), [#222](https://github.com/saveenr/VisioAutomation/issues/222) and [#219](https://github.com/saveenr/VisioAutomation/issues/219), and the 23 tests in `DirectedGraphXmlOptionsTests` for [#225](https://github.com/saveenr/VisioAutomation/issues/225). +- **Tests:** 328 tests across four projects (VTest 108, VTest.Models 104, VTest.Scripting 43, VTest.PowerShell 73). The last full Release run (256) was on 2026-09-30; the newest tests are the regression tests for [#197](https://github.com/saveenr/VisioAutomation/issues/197), [#222](https://github.com/saveenr/VisioAutomation/issues/222) and [#219](https://github.com/saveenr/VisioAutomation/issues/219), the 23 tests in `DirectedGraphXmlOptionsTests` for [#225](https://github.com/saveenr/VisioAutomation/issues/225), and the 46 cmdlet-binding tests of the second slice of [#173](https://github.com/saveenr/VisioAutomation/issues/173). - **Docs:** the .NET GitBook's Diagram models section was reorganized on 2026-10-01 (docs repo PRs 8 and 9, live). Every model page now has a "Where the output goes" section; the DOM page has a render performance page beneath it. The 2026-09 entries on the docs-updates page record each change. The "Models docs follow-ups" entry in [`docs/futures/docs.md`](docs/futures/docs.md) lists which doc statements to revisit when each source issue below is decided, and the release-gated "unreleased" notes to turn into version statements at the next release. The [#219](https://github.com/saveenr/VisioAutomation/issues/219) fix and the new directed graph XML options ([#225](https://github.com/saveenr/VisioAutomation/issues/225)) are described in the docs as unreleased changes after 3.1.0; the fixes for [#197](https://github.com/saveenr/VisioAutomation/issues/197) and [#222](https://github.com/saveenr/VisioAutomation/issues/222) are not yet described. - **Docs conventions:** no em-dashes in either GitBook. The docs repos publish when a PR merges to `main`, and each PR gets a GitBook preview. Cite the issue number on any page that describes behavior an open issue may change. - **Docs viewer:** the local GitBook viewer now lives in its own private repo, `TheSevenPens/LocalGitbookviewer` (cloned at `C:/Users/savee/Documents/GitHub/LocalGitbookviewer`). Run `view.cmd ` from that checkout. Its backlog is in that repo's `BACKLOG.md`. @@ -188,7 +188,7 @@ Roughly in the order worth doing. Pick from the top. ([#219](https://github.com/ 1. **Decide the three public types with no real caller, together:** [#218](https://github.com/saveenr/VisioAutomation/issues/218) Box layout, [#220](https://github.com/saveenr/VisioAutomation/issues/220) form page model, [#221](https://github.com/saveenr/VisioAutomation/issues/221) `ContainerLayout` (draws plain rectangles; the `ContainerMaster` option is never read). The options are in each issue; the docs side of each is listed in the "Models docs follow-ups" backlog entry. 2. **Cut the next release when ready.** Versions are not decided (fixes only, so likely 3.1.1 and a PS module patch). The flow is in [`docs/futures/releases.md`](docs/futures/releases.md). At release time also turn the "unreleased" notes in both GitBooks into version statements and add the new versions to both version-compatibility pages. 3. **Design discussions, not yet quick:** [#223](https://github.com/saveenr/VisioAutomation/issues/223) `client.Model` draw methods are inconsistent, [#225](https://github.com/saveenr/VisioAutomation/issues/225) the rest of the directed graph XML options (the first tier shipped in the unreleased changes; per-edge master and stencil, renderer and styling from XML, and an XSD remain), [#229](https://github.com/saveenr/VisioAutomation/issues/229) `NewDocumentFromTemplate` does not base the document on the template (affects `New-VisioDocument -Template`), and [#198](https://github.com/saveenr/VisioAutomation/issues/198) / [#199](https://github.com/saveenr/VisioAutomation/issues/199) drawing a single directed graph onto the current page. -4. **Earlier backlog:** [#191](https://github.com/saveenr/VisioAutomation/issues/191) migrate cmdlets to `ThrowTerminatingError` (makes `InvokeScriptStrict` redundant), [#190](https://github.com/saveenr/VisioAutomation/issues/190) the `VTest.PowerShell` two-Client mismatch, [#183](https://github.com/saveenr/VisioAutomation/issues/183) VisioScripting dead-method removal (CY27Q1; re-verify zero callers first), [#173](https://github.com/saveenr/VisioAutomation/issues/173) more cmdlet-binding tests (about 50 cmdlets still untested), [#163](https://github.com/saveenr/VisioAutomation/issues/163) and [#164](https://github.com/saveenr/VisioAutomation/issues/164) (the `PSCmdlet` question; see the memory note on why cmdlets derive from `Cmdlet`). +4. **Earlier backlog:** [#191](https://github.com/saveenr/VisioAutomation/issues/191) migrate cmdlets to `ThrowTerminatingError` (makes `InvokeScriptStrict` redundant), [#190](https://github.com/saveenr/VisioAutomation/issues/190) the `VTest.PowerShell` two-Client mismatch, [#183](https://github.com/saveenr/VisioAutomation/issues/183) VisioScripting dead-method removal (CY27Q1; re-verify zero callers first), [#173](https://github.com/saveenr/VisioAutomation/issues/173) more cmdlet-binding tests (the second slice covered 13 cmdlets; roughly 40 are still untested, see `docs/futures/test-coverage-gaps.md`), [#232](https://github.com/saveenr/VisioAutomation/issues/232) `Get-VisioPage -ID` uses the page index, not the page ID, [#163](https://github.com/saveenr/VisioAutomation/issues/163) and [#164](https://github.com/saveenr/VisioAutomation/issues/164) (the `PSCmdlet` question; see the memory note on why cmdlets derive from `Cmdlet`). **Calendar-bound** diff --git a/VisioAutomation_2010/VTest.PowerShell/CmdletScriptExtensions.cs b/VisioAutomation_2010/VTest.PowerShell/CmdletScriptExtensions.cs new file mode 100644 index 000000000..0c38b5df3 --- /dev/null +++ b/VisioAutomation_2010/VTest.PowerShell/CmdletScriptExtensions.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using MUT = Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace VTest.PowerShell +{ + // Helpers for cmdlet-binding tests that run whole scripts inside the session's runspace, so the + // cmdlets go through PowerShell's parameter binder (see CmdletBindingTests for why that matters). + // Everything a test needs is created inside the script and plain values are returned, which + // keeps COM objects from crossing between the script and the test. + public static class CmdletScriptExtensions + { + // Runs "body" in a fresh Visio document and returns what it writes to the pipeline. + // The document is closed afterward without a save prompt. + public static List RunInNewDocument(this VisioPSSession session, string body) + { + string script = + "$doc = New-VisioDocument; " + + "try { " + body + " } " + + "finally { try { $doc.Saved = $true; $doc.Close() } catch { } }"; + return session.InvokeScriptStrict(script); + } + + // Runs "body" in a fresh Visio document where the script is expected to fail. Returns the + // exception, which is flattened into its message chain by MessageOf. Fails the test if it succeeds. + public static System.Exception ExpectFailureInNewDocument(this VisioPSSession session, string body) + { + try + { + session.RunInNewDocument(body); + } + catch (System.Exception ex) + { + return ex; + } + + MUT.Assert.Fail("Expected the script to fail: " + body); + return null; + } + + // The messages of an exception and everything inside it, so a test can look for a phrase + // without caring how many layers PowerShell wrapped the error in. + public static string MessageOf(System.Exception ex) + { + var parts = new List(); + while (ex != null) + { + parts.Add(ex.GetType().Name + ": " + ex.Message); + ex = ex.InnerException; + } + return string.Join(" | ", parts); + } + } +} diff --git a/VisioAutomation_2010/VTest.PowerShell/DocumentWindowCmdletBindingTests.cs b/VisioAutomation_2010/VTest.PowerShell/DocumentWindowCmdletBindingTests.cs new file mode 100644 index 000000000..b4f67f03d --- /dev/null +++ b/VisioAutomation_2010/VTest.PowerShell/DocumentWindowCmdletBindingTests.cs @@ -0,0 +1,120 @@ +using System.Linq; +using VisioPowerShell.Commands.VisioApplication; +using MUT = Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace VTest.PowerShell +{ + // Second slice of #173: parameter binding for Get-VisioDocument, Format-VisioWindow and the + // pipeline-driven parameter sets of Out-VisioApplication. + [MUT.TestClass] + public class DocumentWindowCmdletBindingTests + { + private static readonly VisioPSSession Session = new VisioPSSession(); + + [MUT.ClassInitialize] + public static void ClassInitialize(MUT.TestContext context) + { + var new_visio_application = new NewVisioApplication(); + + // Out-VisioApplication's input types are created with New-Object in the scripts below, which only + // finds assemblies that are already loaded. + var models_assembly = typeof(VisioAutomation.Models.Data.DataTableModel).Assembly; + } + + [MUT.ClassCleanup] + public static void ClassCleanup() + { + try { DocumentWindowCmdletBindingTests.Session.Cmd_Close_VisioApplication(true); } + catch (System.Exception) { } + DocumentWindowCmdletBindingTests.Session.CleanUp(); + } + + // -- Get-VisioDocument: parameter sets ----------------------------------------- + + [MUT.TestMethod] + public void GetVisioDocument_ActiveDocument_ReturnsTheActiveDocument() + { + var same = DocumentWindowCmdletBindingTests.Session.RunInNewDocument( + "(Get-VisioDocument -ActiveDocument).Name -eq $doc.Name"); + MUT.Assert.IsTrue(same.Single()); + } + + [MUT.TestMethod] + public void GetVisioDocument_NoArguments_IncludesTheNewDocument() + { + var found = DocumentWindowCmdletBindingTests.Session.RunInNewDocument( + "(Get-VisioDocument | ForEach-Object { $_.Name }) -contains $doc.Name"); + MUT.Assert.IsTrue(found.Single()); + } + + [MUT.TestMethod] + public void GetVisioDocument_PositionalName_FiltersByName() + { + var names = DocumentWindowCmdletBindingTests.Session.RunInNewDocument( + "(Get-VisioDocument $doc.Name).Name"); + MUT.Assert.AreEqual(1, names.Count); + } + + [MUT.TestMethod] + public void GetVisioDocument_ActiveDocumentWithName_FailsToResolveAParameterSet() + { + var ex = DocumentWindowCmdletBindingTests.Session.ExpectFailureInNewDocument("Get-VisioDocument -ActiveDocument -Name 'x'"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Parameter set cannot be resolved"); + } + + // -- Format-VisioWindow: three parameter sets ---------------------------------- + + [MUT.TestMethod] + public void FormatVisioWindow_Zoom_SetsTheWindowZoom() + { + var zoom = DocumentWindowCmdletBindingTests.Session.RunInNewDocument( + "Format-VisioWindow -Zoom 0.5; (Get-VisioApplication).ActiveWindow.Zoom"); + MUT.Assert.AreEqual(0.5, zoom.Single(), 1e-9); + } + + [MUT.TestMethod] + public void FormatVisioWindow_ZoomAndZoomTo_FailToResolveAParameterSet() + { + var ex = DocumentWindowCmdletBindingTests.Session.ExpectFailureInNewDocument("Format-VisioWindow -Zoom 1 -ZoomTo Page"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Parameter set cannot be resolved"); + } + + [MUT.TestMethod] + public void FormatVisioWindow_UnknownZoomTo_FailsToBind() + { + var ex = DocumentWindowCmdletBindingTests.Session.ExpectFailureInNewDocument("Format-VisioWindow -ZoomTo Bogus"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Cannot bind parameter 'ZoomTo'"); + } + + // -- Out-VisioApplication: the parameter set is chosen by the type of the piped object ---- + + [MUT.TestMethod] + public void OutVisioApplication_PipedDataTableModel_DrawsTheTable() + { + var counts = DocumentWindowCmdletBindingTests.Session.RunInNewDocument( + "$dt = New-Object System.Data.DataTable; $null = $dt.Columns.Add('A'); $null = $dt.Columns.Add('B'); $null = $dt.Rows.Add('1', '2'); " + + "$m = New-Object VisioAutomation.Models.Data.DataTableModel; $m.DataTable = $dt; " + + "$m | Out-VisioApplication; (Get-VisioShape | Measure-Object).Count"); + MUT.Assert.AreEqual(2, counts.Single(), "one row of two columns is two cell shapes"); + } + + [MUT.TestMethod] + public void OutVisioApplication_PipedXmlModel_DrawsTheTree() + { + var counts = DocumentWindowCmdletBindingTests.Session.RunInNewDocument( + "$m = New-Object VisioAutomation.Models.Data.XmlModel; $m.XmlDocument = [xml]''; " + + "$m | Out-VisioApplication; (Get-VisioShape | Measure-Object).Count"); + MUT.Assert.AreEqual(5, counts.Single(), "three element nodes and two connectors"); + } + + [MUT.TestMethod] + public void OutVisioApplication_PipedStringIsNotAModel_FailsToBind() + { + var ex = DocumentWindowCmdletBindingTests.Session.ExpectFailureInNewDocument("'not a model' | Out-VisioApplication"); + string message = CmdletScriptExtensions.MessageOf(ex); + MUT.Assert.IsTrue( + message.Contains("Parameter set cannot be resolved") || message.Contains("cannot be bound"), + "expected a binding error, got: " + message); + } + } +} diff --git a/VisioAutomation_2010/VTest.PowerShell/PageCmdletBindingTests.cs b/VisioAutomation_2010/VTest.PowerShell/PageCmdletBindingTests.cs new file mode 100644 index 000000000..7d07fc81b --- /dev/null +++ b/VisioAutomation_2010/VTest.PowerShell/PageCmdletBindingTests.cs @@ -0,0 +1,159 @@ +using System.Linq; +using VisioPowerShell.Commands.VisioApplication; +using MUT = Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace VTest.PowerShell +{ + // Second slice of #173: parameter binding for the page cmdlets (Get-VisioPage, New-VisioPage, + // Format-VisioPage, Remove-VisioPage). Each test runs a script in the session's runspace so + // PowerShell's binder resolves parameter sets, positions, switches and enums. + [MUT.TestClass] + public class PageCmdletBindingTests + { + private static readonly VisioPSSession Session = new VisioPSSession(); + + [MUT.ClassInitialize] + public static void ClassInitialize(MUT.TestContext context) + { + var new_visio_application = new NewVisioApplication(); + } + + [MUT.ClassCleanup] + public static void ClassCleanup() + { + try { PageCmdletBindingTests.Session.Cmd_Close_VisioApplication(true); } + catch (System.Exception) { } + PageCmdletBindingTests.Session.CleanUp(); + } + + private static string page_size(string page_expression) + { + return "\"$(" + page_expression + ".PageSheet.CellsU('PageWidth').ResultIU) x $(" + page_expression + ".PageSheet.CellsU('PageHeight').ResultIU)\""; + } + + // -- Get-VisioPage: parameter sets ----------------------------------------- + + [MUT.TestMethod] + public void GetVisioPage_NoArguments_ReturnsEveryPageInTheDocument() + { + var counts = PageCmdletBindingTests.Session.RunInNewDocument( + "$null = New-VisioPage -Name 'second'; (Get-VisioPage | Measure-Object).Count"); + MUT.Assert.AreEqual(2, counts.Single()); + } + + [MUT.TestMethod] + public void GetVisioPage_PositionalName_FiltersByName() + { + var names = PageCmdletBindingTests.Session.RunInNewDocument( + "$null = New-VisioPage -Name 'second'; (Get-VisioPage 'second').Name"); + MUT.Assert.AreEqual("second", names.Single()); + } + + [MUT.TestMethod] + public void GetVisioPage_ActivePage_ReturnsTheActivePage() + { + var same = PageCmdletBindingTests.Session.RunInNewDocument( + "$null = New-VisioPage -Name 'second'; " + + "(Get-VisioPage -ActivePage).Name -eq (Get-VisioApplication).ActivePage.Name"); + MUT.Assert.IsTrue(same.Single()); + } + + [MUT.TestMethod] + public void GetVisioPage_ActivePageWithName_FailsToResolveAParameterSet() + { + var ex = PageCmdletBindingTests.Session.ExpectFailureInNewDocument("Get-VisioPage -ActivePage -Name 'x'"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Parameter set cannot be resolved"); + } + + // -- New-VisioPage ---------------------------------------------------------- + + [MUT.TestMethod] + public void NewVisioPage_Name_SetsThePageName() + { + var names = PageCmdletBindingTests.Session.RunInNewDocument("(New-VisioPage -Name 'Overview').Name"); + MUT.Assert.AreEqual("Overview", names.Single()); + } + + [MUT.TestMethod] + public void NewVisioPage_EmptyName_Throws() + { + var ex = PageCmdletBindingTests.Session.ExpectFailureInNewDocument("New-VisioPage -Name ''"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Name can't be empty"); + } + + [MUT.TestMethod] + public void NewVisioPage_WhitespaceName_Throws() + { + var ex = PageCmdletBindingTests.Session.ExpectFailureInNewDocument("New-VisioPage -Name ' '"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Name can't be empty"); + } + + [MUT.TestMethod] + public void NewVisioPage_WidthAndHeight_SetThePageSize() + { + var sizes = PageCmdletBindingTests.Session.RunInNewDocument( + "$p = New-VisioPage -Width 3 -Height 2; " + page_size("$p")); + MUT.Assert.AreEqual("3 x 2", sizes.Single()); + } + + // -- Format-VisioPage ------------------------------------------------------- + + [MUT.TestMethod] + public void FormatVisioPage_WidthAndHeight_SetThePageSize() + { + // Regression: SetFormatCells committed the writes to the Page instead of its page sheet, + // so Format-VisioPage -Width / -Height always threw a COMException. + var sizes = PageCmdletBindingTests.Session.RunInNewDocument( + "$p = New-VisioPage -Name 'f'; Format-VisioPage -Page $p -Width 5 -Height 4; " + page_size("$p")); + MUT.Assert.AreEqual("5 x 4", sizes.Single()); + } + + [MUT.TestMethod] + public void FormatVisioPage_OnlyWidth_LeavesTheHeightAlone() + { + var sizes = PageCmdletBindingTests.Session.RunInNewDocument( + "$p = New-VisioPage -Width 3 -Height 2; Format-VisioPage -Page $p -Width 6; " + page_size("$p")); + MUT.Assert.AreEqual("6 x 2", sizes.Single()); + } + + [MUT.TestMethod] + public void FormatVisioPage_Landscape_MakesThePageWiderThanItIsTall() + { + var wider = PageCmdletBindingTests.Session.RunInNewDocument( + "$p = New-VisioPage -Width 4 -Height 6; Format-VisioPage -Page $p -Orientation Landscape; " + + "$p.PageSheet.CellsU('PageWidth').ResultIU -gt $p.PageSheet.CellsU('PageHeight').ResultIU"); + MUT.Assert.IsTrue(wider.Single()); + } + + [MUT.TestMethod] + public void FormatVisioPage_UnknownOrientation_FailsToBind() + { + var ex = PageCmdletBindingTests.Session.ExpectFailureInNewDocument("Format-VisioPage -Orientation Sideways"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Cannot bind parameter 'Orientation'"); + } + + [MUT.TestMethod] + public void FormatVisioPage_FitContents_ShrinksThePageAroundTheShapes() + { + var widths = PageCmdletBindingTests.Session.RunInNewDocument( + "$p = New-VisioPage -Width 20 -Height 20; " + + "$null = New-VisioShape -Rectangle -BoundingBox (New-VisioRectangle 2 2 3 3); " + + "Format-VisioPage -Page $p -FitContents -BorderWidth 0.5 -BorderHeight 0.5; " + + "$p.PageSheet.CellsU('PageWidth').ResultIU"); + MUT.Assert.IsTrue(widths.Single() < 20.0, "the page should shrink to fit its contents, got " + widths.Single()); + MUT.Assert.IsTrue(widths.Single() >= 1.0, "the page should still hold the shape, got " + widths.Single()); + } + + // -- Remove-VisioPage ------------------------------------------------------- + + [MUT.TestMethod] + public void RemoveVisioPage_PipedPage_RemovesThatPage() + { + var counts = PageCmdletBindingTests.Session.RunInNewDocument( + "$p = New-VisioPage -Name 'gone'; $before = (Get-VisioPage | Measure-Object).Count; " + + "$p | Remove-VisioPage; $after = (Get-VisioPage | Measure-Object).Count; " + + "\"$before -> $after\""); + MUT.Assert.AreEqual("2 -> 1", counts.Single()); + } + } +} diff --git a/VisioAutomation_2010/VTest.PowerShell/ShapeCmdletBindingTests.cs b/VisioAutomation_2010/VTest.PowerShell/ShapeCmdletBindingTests.cs new file mode 100644 index 000000000..c65e22950 --- /dev/null +++ b/VisioAutomation_2010/VTest.PowerShell/ShapeCmdletBindingTests.cs @@ -0,0 +1,245 @@ +using System.Linq; +using VisioPowerShell.Commands.VisioApplication; +using MUT = Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace VTest.PowerShell +{ + // Second slice of #173: parameter binding for the shape cmdlets (Get-VisioShape, Select-VisioShape, + // Format-VisioShape, New-VisioHyperlink, Set-VisioCustomProperty, Connect-VisioShape). + [MUT.TestClass] + public class ShapeCmdletBindingTests + { + private static readonly VisioPSSession Session = new VisioPSSession(); + + // Draws a 1 x 1 rectangle with its lower left corner at (x, y) on the active page. + private static string rect(string variable, double x, double y) + { + return string.Format( + System.Globalization.CultureInfo.InvariantCulture, + "{0} = New-VisioShape -Rectangle -BoundingBox (New-VisioRectangle {1} {2} {3} {4}); ", + variable, x, y, x + 1, y + 1); + } + + [MUT.ClassInitialize] + public static void ClassInitialize(MUT.TestContext context) + { + var new_visio_application = new NewVisioApplication(); + } + + [MUT.ClassCleanup] + public static void ClassCleanup() + { + try { ShapeCmdletBindingTests.Session.Cmd_Close_VisioApplication(true); } + catch (System.Exception) { } + ShapeCmdletBindingTests.Session.CleanUp(); + } + + // -- Get-VisioShape: parameter sets ---------------------------------------- + + [MUT.TestMethod] + public void GetVisioShape_NoArguments_ReturnsEveryShapeOnThePage() + { + var counts = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$a", 0, 0) + rect("$b", 3, 0) + "(Get-VisioShape | Measure-Object).Count"); + MUT.Assert.AreEqual(2, counts.Single()); + } + + [MUT.TestMethod] + public void GetVisioShape_ID_ReturnsTheShapeWithThatShapeID() + { + var ok = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$a", 0, 0) + rect("$b", 3, 0) + "(Get-VisioShape -ID $b.ID).ID -eq $b.ID"); + MUT.Assert.IsTrue(ok.Single()); + } + + [MUT.TestMethod] + public void GetVisioShape_ActiveSelectionWithName_FailsToResolveAParameterSet() + { + var ex = ShapeCmdletBindingTests.Session.ExpectFailureInNewDocument("Get-VisioShape -ActiveSelection -Name 'x'"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Parameter set cannot be resolved"); + } + + // -- Select-VisioShape: two required parameter sets -------------------------- + + [MUT.TestMethod] + public void SelectVisioShape_SelectAll_SelectsEveryShape() + { + var counts = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$a", 0, 0) + rect("$b", 3, 0) + + "Select-VisioShape -SelectionOperation SelectAll; (Get-VisioShape -ActiveSelection | Measure-Object).Count"); + MUT.Assert.AreEqual(2, counts.Single()); + } + + [MUT.TestMethod] + public void SelectVisioShape_SelectNone_ClearsTheSelection() + { + var counts = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$a", 0, 0) + "Select-VisioShape -SelectionOperation SelectAll; Select-VisioShape -SelectionOperation SelectNone; " + + "(Get-VisioShape -ActiveSelection | Measure-Object).Count"); + MUT.Assert.AreEqual(0, counts.Single()); + } + + [MUT.TestMethod] + public void SelectVisioShape_ShapesParameter_SelectsJustThoseShapes() + { + var counts = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$a", 0, 0) + rect("$b", 3, 0) + + "Select-VisioShape -Shapes $b; (Get-VisioShape -ActiveSelection | Measure-Object).Count"); + MUT.Assert.AreEqual(1, counts.Single()); + } + + [MUT.TestMethod] + public void SelectVisioShape_WithNoArguments_FailsToResolveAParameterSet() + { + var ex = ShapeCmdletBindingTests.Session.ExpectFailureInNewDocument("Select-VisioShape"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Parameter set cannot be resolved"); + } + + [MUT.TestMethod] + public void SelectVisioShape_ShapesAndSelectionOperationTogether_FailToResolveAParameterSet() + { + var ex = ShapeCmdletBindingTests.Session.ExpectFailureInNewDocument( + rect("$a", 0, 0) + "Select-VisioShape -Shapes $a -SelectionOperation SelectAll"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Parameter set cannot be resolved"); + } + + [MUT.TestMethod] + public void SelectVisioShape_UnknownOperation_FailsToBind() + { + var ex = ShapeCmdletBindingTests.Session.ExpectFailureInNewDocument("Select-VisioShape -SelectionOperation Bogus"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Cannot bind parameter 'SelectionOperation'"); + } + + // -- Format-VisioShape: numeric, switch and enum parameters ------------------- + + [MUT.TestMethod] + public void FormatVisioShape_NudgeX_MovesTheShapeByThatDistance() + { + var moved = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$a", 0, 0) + + "$x0 = $a.CellsU('PinX').ResultIU; Format-VisioShape -Shape $a -NudgeX 2; $a.CellsU('PinX').ResultIU - $x0"); + MUT.Assert.AreEqual(2.0, moved.Single(), 1e-9); + } + + [MUT.TestMethod] + public void FormatVisioShape_AlignHorizontalLeft_AlignsTheShapesLeftEdges() + { + var pins = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$a", 0, 2) + rect("$b", 3, 2) + + "Format-VisioShape -Shape $a,$b -AlignHorizontal Left; " + + "\"$($a.CellsU('PinX').ResultIU),$($b.CellsU('PinX').ResultIU)\""); + string[] parts = pins.Single().Split(','); + MUT.Assert.AreEqual(double.Parse(parts[0]), double.Parse(parts[1]), 1e-9, "both shapes should end up in the same column"); + } + + [MUT.TestMethod] + public void FormatVisioShape_DistributeHorizontalSwitch_SpacesTheShapesEvenly() + { + var pins = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$a", 0, 0) + rect("$b", 1, 0) + rect("$c", 6, 0) + + "Format-VisioShape -Shape $a,$b,$c -DistributeHorizontal; " + + "\"$($a.CellsU('PinX').ResultIU),$($b.CellsU('PinX').ResultIU),$($c.CellsU('PinX').ResultIU)\""); + double[] x = pins.Single().Split(',').Select(double.Parse).ToArray(); + MUT.Assert.AreEqual(x[1] - x[0], x[2] - x[1], 1e-9, "the gaps between the three shapes should be equal"); + } + + [MUT.TestMethod] + public void FormatVisioShape_UnknownAlignment_FailsToBind() + { + var ex = ShapeCmdletBindingTests.Session.ExpectFailureInNewDocument("Format-VisioShape -AlignHorizontal Diagonal"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Cannot bind parameter 'AlignHorizontal'"); + } + + // -- New-VisioHyperlink: mandatory Address and boolean parameters ------------ + + [MUT.TestMethod] + public void NewVisioHyperlink_Address_AddsAHyperlinkToTheShape() + { + var results = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$s", 0, 0) + + "New-VisioHyperlink -Address 'https://example.com' -Description 'the site' -Shape $s; " + + "\"$($s.Hyperlinks.Count)|$($s.Hyperlinks.Item(0).Address)|$($s.Hyperlinks.Item(0).Description)\""); + MUT.Assert.AreEqual("1|https://example.com|the site", results.Single()); + } + + [MUT.TestMethod] + public void NewVisioHyperlink_WithoutAnAddress_ReportsTheMissingMandatoryParameter() + { + var ex = ShapeCmdletBindingTests.Session.ExpectFailureInNewDocument(rect("$s", 0, 0) + "New-VisioHyperlink -Shape $s"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Address"); + } + + [MUT.TestMethod] + public void NewVisioHyperlink_NewWindowDefaultAndInvisible_SetTheHyperlinkCells() + { + // Regression: these parameters bound but were dropped when the hyperlink was added, + // so the cells always stayed FALSE. + var results = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$s", 0, 0) + + "New-VisioHyperlink -Address 'https://example.com' -NewWindow $true -Default $true -Invisible $true -Shape $s; " + + "\"$($s.CellsU('Hyperlink.Row_1.NewWindow').FormulaU),$($s.CellsU('Hyperlink.Row_1.Default').FormulaU),$($s.CellsU('Hyperlink.Row_1.Invisible').FormulaU)\""); + MUT.Assert.AreEqual("TRUE,TRUE,TRUE", results.Single()); + } + + [MUT.TestMethod] + public void NewVisioHyperlink_WithoutTheBooleanParameters_LeavesTheCellsFalse() + { + var results = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$s", 0, 0) + + "New-VisioHyperlink -Address 'https://example.com' -Shape $s; " + + "\"$($s.CellsU('Hyperlink.Row_1.NewWindow').FormulaU),$($s.CellsU('Hyperlink.Row_1.Default').FormulaU),$($s.CellsU('Hyperlink.Row_1.Invisible').FormulaU)\""); + MUT.Assert.AreEqual("FALSE,FALSE,FALSE", results.Single()); + } + + [MUT.TestMethod] + public void NewVisioHyperlink_SortKey_SetsTheSortKeyCell() + { + var results = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$s", 0, 0) + + "New-VisioHyperlink -Address 'https://example.com' -SortKey 'b-key' -Shape $s; " + + "$s.CellsU('Hyperlink.Row_1.SortKey').ResultStr(0)"); + MUT.Assert.AreEqual("b-key", results.Single()); + } + + // -- Set-VisioCustomProperty: two parameter sets ------------------------------- + + [MUT.TestMethod] + public void SetVisioCustomProperty_NamedProperties_SetValueTypeAndLabel() + { + var results = ShapeCmdletBindingTests.Session.RunInNewDocument( + rect("$s", 0, 0) + + "Set-VisioCustomProperty 'cost' 2.5 -Type 2 -Label 'Cost' -Shape $s; " + + "\"$($s.CellsU('Prop.cost').FormulaU)|$($s.CellsU('Prop.cost.Type').ResultIU)|$($s.CellsU('Prop.cost.Label').FormulaU)\""); + MUT.Assert.AreEqual("2.5|2|\"Cost\"", results.Single()); + } + + [MUT.TestMethod] + public void SetVisioCustomProperty_CellsAndValueTogether_FailToResolveAParameterSet() + { + var ex = ShapeCmdletBindingTests.Session.ExpectFailureInNewDocument( + rect("$s", 0, 0) + + "$c = New-Object VisioAutomation.Shapes.CustomPropertyCells; Set-VisioCustomProperty 'p' -Cells $c -Value 1 -Shape $s"); + MUT.StringAssert.Contains(CmdletScriptExtensions.MessageOf(ex), "Parameter set cannot be resolved"); + } + + [MUT.TestMethod] + public void SetVisioCustomProperty_WithoutAName_FailsToBind() + { + var ex = ShapeCmdletBindingTests.Session.ExpectFailureInNewDocument("Set-VisioCustomProperty"); + MUT.Assert.IsTrue(CmdletScriptExtensions.MessageOf(ex).Contains("Parameter set cannot be resolved") || + CmdletScriptExtensions.MessageOf(ex).Contains("missing mandatory"), + "expected a binding error, got: " + CmdletScriptExtensions.MessageOf(ex)); + } + + // -- Connect-VisioShape: mandatory From and To --------------------------------- + + [MUT.TestMethod] + public void ConnectVisioShape_WithoutFromAndTo_ReportsTheMissingMandatoryParameters() + { + var ex = ShapeCmdletBindingTests.Session.ExpectFailureInNewDocument("Connect-VisioShape"); + string message = CmdletScriptExtensions.MessageOf(ex); + MUT.StringAssert.Contains(message, "From"); + MUT.StringAssert.Contains(message, "To"); + } + } +} diff --git a/docs/TESTING.md b/docs/TESTING.md index 38452e6cc..7cefc60f7 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -11,7 +11,7 @@ All under `VisioAutomation_2010/`: | `VTest` | 108 | `VisioAutomation` (core) | [VTest/README.md](../VisioAutomation_2010/VTest/README.md) | | `VTest.Models` | 104 | `VisioAutomation.Models` (DOM, layouts) | [VTest.Models/README.md](../VisioAutomation_2010/VTest.Models/README.md) | | `VTest.Scripting` | 43 | `VisioScripting` (high-level facade) | [VTest.Scripting/README.md](../VisioAutomation_2010/VTest.Scripting/README.md) | -| `VTest.PowerShell` | 27 | `VisioPowerShell` (cmdlets) | [VTest.PowerShell/README.md](../VisioAutomation_2010/VTest.PowerShell/README.md) | +| `VTest.PowerShell` | 73 | `VisioPowerShell` (cmdlets) | [VTest.PowerShell/README.md](../VisioAutomation_2010/VTest.PowerShell/README.md) | 256 tests passed with no skips on 2026-09-30 (Release build of `master` at `581ad539`). Three regression tests (`DrawXmlModel_IsUndoneByASingleUndo`, `DrawDirectedGraphDocument_UsesTheEdgeMasterFromTheStylingParameter` and `DrawOrgChart_DrawsInANewDocumentAndLeavesTheTargetPageAlone`) and the 23 tests in `DirectedGraphXmlOptionsTests` have since been added to `VTest.Models`; Debug runs of `VTest.Models`, `VTest.Scripting` and `VTest.PowerShell` on 2026-10-01 passed 104, 43 and 27 tests. Counts come from the Release TRX. [HANDOVER.md](HANDOVER.md) records the environment and the original evidence, taken at 237 tests on 2026-09-29. @@ -84,6 +84,10 @@ The harness registers the exact test-build assembly before opening the runspace - Use **`InvokeScriptStrict`** for tests that expect the cmdlet to throw (so the test can catch the propagated exception), or that want any unexpected error from the cmdlet to surface as a test failure rather than be silently dropped on the error stream. **This is the right default for cmdlet-binding tests** ([`CmdletBindingTests.cs`](../VisioAutomation_2010/VTest.PowerShell/CmdletBindingTests.cs) is the canonical example). - Use plain **`InvokeScript`** when the cmdlet is allowed to write non-fatal records to the error stream and the test only cares about the success-path return value. +#### Script-driven tests: `RunInNewDocument` and `ExpectFailureInNewDocument` + +The second slice of binding tests ([#173](https://github.com/saveenr/VisioAutomation/issues/173): `PageCmdletBindingTests`, `ShapeCmdletBindingTests` and `DocumentWindowCmdletBindingTests`) builds everything inside one script and returns plain values (counts, strings, numbers) so that no COM objects cross between the script and the test. The extension methods in [`CmdletScriptExtensions`](../VisioAutomation_2010/VTest.PowerShell/CmdletScriptExtensions.cs) support that: `RunInNewDocument(body)` creates a document, runs the body through `InvokeScriptStrict`, and closes the document without a save prompt; `ExpectFailureInNewDocument(body)` returns the exception from a body that must fail, and `MessageOf` flattens its message chain so a test can look for a phrase such as `Parameter set cannot be resolved` or `Cannot bind parameter 'Orientation'`. Two things these tests rely on: a script run in this runspace never prompts for a missing mandatory parameter (it fails with a binding error instead), and `New-Object` in a script only finds assemblies that are already loaded, so a test class that creates model types touches the assembly in `ClassInitialize`. + The longer-term cleanup (migrating cmdlets from raw `throw` to `ThrowTerminatingError(ErrorRecord)`, which always propagates regardless of `$ErrorActionPreference`) is tracked in [#191](https://github.com/saveenr/VisioAutomation/issues/191). Until that lands, `InvokeScriptStrict` is the correct workaround for binding-test exception assertions. ## Quality gates diff --git a/docs/futures/test-coverage-gaps.md b/docs/futures/test-coverage-gaps.md index 049b35dc1..7757bd4f2 100644 --- a/docs/futures/test-coverage-gaps.md +++ b/docs/futures/test-coverage-gaps.md @@ -68,6 +68,8 @@ The 56 cmdlets without a `VTest.PowerShell\BasicTests` test, grouped by surface | VisioUserDefinedCell | `Remove-VisioUserDefinedCell` | | VisioWindow | `Format-VisioWindow` | +**Second slice done (2026-10-01):** `Get-VisioPage`, `Get-VisioShape`, `Get-VisioDocument`, `Select-VisioShape`, `New-VisioPage`, `Format-VisioPage`, `Remove-VisioPage`, `Format-VisioShape`, `New-VisioHyperlink`, `Set-VisioCustomProperty`, `Connect-VisioShape` (mandatory parameters only), `Format-VisioWindow` and the type-driven parameter sets of `Out-VisioApplication` now have binding tests (`PageCmdletBindingTests`, `ShapeCmdletBindingTests`, `DocumentWindowCmdletBindingTests`). They found two real bugs, both fixed (`Format-VisioPage -Width/-Height` and `New-VisioHyperlink -NewWindow/-Default/-Invisible/-SortKey`), and one open question ([#232](https://github.com/saveenr/VisioAutomation/issues/232): `Get-VisioPage -ID` uses the page index, not the page ID). + The four cmdlets fixed in 4.6.1 — `Lock-VisioShape`, `Unlock-VisioShape`, `Export-VisioShape`, `New-VisioShape` — and `Connect-VisioShape` (pipeline parameter set, [#163](https://github.com/saveenr/VisioAutomation/issues/163), parked behind [#164](https://github.com/saveenr/VisioAutomation/issues/164)) are the highest-priority targets within this list. Their bug histories are evidence the binding surface needs guarding. ### `VisioScripting/Commands` (no dedicated test file)