diff --git a/README.md b/README.md index 85123cb..eb4e0a8 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,7 @@ A tool that answers everything is easy to build. This is the other kind. ### Installing it -**[The add-in's page →](https://peopleworks.github.io/SignsofAI/word)** — what it does, both +**[The add-in's page →](https://peopleworks.github.io/SignsofAI/word-addin)** — what it does, both install routes, and what it refuses to do, in English and Spanish. The short version: on the web it is Home → Add-ins → More Add-ins → My Add-ins → **Upload My Add-in** diff --git a/src/SignsOfAI.UI/Layout/MainLayout.razor b/src/SignsOfAI.UI/Layout/MainLayout.razor index 1a28574..6984f0e 100644 --- a/src/SignsOfAI.UI/Layout/MainLayout.razor +++ b/src/SignsOfAI.UI/Layout/MainLayout.razor @@ -24,7 +24,7 @@ @L["nav.catalog"] @* No host condition, unlike the two below: somebody using the desktop app writes in Word too, and the add-in is useful to them for the same reasons. *@ - @L["nav.word"] + @L["nav.word"] @* Only where there is something to gain by downloading it. The same signal as the folder link above, and for the same reason: this host cannot open a folder, so a better host exists for this machine and hiding that would be the lowest-common- diff --git a/src/SignsOfAI.UI/Pages/Download.razor b/src/SignsOfAI.UI/Pages/Download.razor index cffcd10..d112919 100644 --- a/src/SignsOfAI.UI/Pages/Download.razor +++ b/src/SignsOfAI.UI/Pages/Download.razor @@ -113,7 +113,7 @@ else

@L["dl.word.title"]

@L["dl.word.lede"]

- @L["dl.word.page"] + @L["dl.word.page"]
diff --git a/src/SignsOfAI.UI/Pages/WordAddin.razor b/src/SignsOfAI.UI/Pages/WordAddin.razor index 7f45b16..c15e143 100644 --- a/src/SignsOfAI.UI/Pages/WordAddin.razor +++ b/src/SignsOfAI.UI/Pages/WordAddin.razor @@ -1,4 +1,7 @@ -@page "/word" +@* /word-addin, not /word: the deploy publishes the task pane itself at /word/, and a directory + on the host wins over a route in the router. The pane's address is in a manifest people have + already installed, so the pane keeps it and the page moves. *@ +@page "/word-addin" @using SignsOfAI.Core.Calibration @inherits LocalizedComponent diff --git a/tests/SignsOfAI.Core.Tests/DeployedPathTests.cs b/tests/SignsOfAI.Core.Tests/DeployedPathTests.cs new file mode 100644 index 0000000..3bf020e --- /dev/null +++ b/tests/SignsOfAI.Core.Tests/DeployedPathTests.cs @@ -0,0 +1,90 @@ +using System.Text.RegularExpressions; +using Xunit; + +namespace SignsOfAI.Core.Tests; + +/// +/// The deploy publishes two applications onto one host: the web app at the site root, and the Word +/// task pane in a subdirectory of it. A real directory beats a client-side route, so any page whose +/// @page path matches that subdirectory is unreachable — the visitor gets the task pane, +/// which outside Word says it has no document to read. +/// +/// That happened. /word was given to a page while the deploy was already publishing the pane +/// at /word/, and the navigation link led to the pane for as long as it took to notice. It is +/// invisible locally, because a dev server has neither directory, and invisible in the diff, because +/// the two halves live in different files. +/// +public class DeployedPathTests +{ + private static string RepoRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !Directory.Exists(Path.Combine(dir.FullName, "src"))) + dir = dir.Parent; + + Assert.NotNull(dir); + return dir!.FullName; + } + + /// Every route the router owns, from the @page directives themselves. + private static IReadOnlyList Routes() + { + string pages = Path.Combine(RepoRoot(), "src", "SignsOfAI.UI", "Pages"); + var routes = new List(); + + foreach (string file in Directory.EnumerateFiles(pages, "*.razor")) + foreach (Match m in Regex.Matches(File.ReadAllText(file), @"^@page\s+""/([^""]*)""", RegexOptions.Multiline)) + routes.Add(m.Groups[1].Value.Trim('/')); + + Assert.NotEmpty(routes); + return routes; + } + + /// + /// The directory the deploy copies the pane into, taken from the workflow rather than repeated + /// here — the point of this test is that the two are compared, not that both are typed twice. + /// + private static string PaneDirectory() + { + string workflow = File.ReadAllText( + Path.Combine(RepoRoot(), ".github", "workflows", "deploy-pages.yml")); + + var m = Regex.Match(workflow, @"cp -r publish-word/wwwroot publish/wwwroot/(\S+)"); + Assert.True(m.Success, + "deploy-pages.yml no longer copies the task pane where this test expects. If the step " + + "moved or was renamed, update this pattern — do not delete the check."); + + return m.Groups[1].Value.Trim('/'); + } + + [Fact] + public void No_page_route_is_shadowed_by_the_task_pane_directory() + { + string pane = PaneDirectory(); + var clash = Routes().FirstOrDefault(r => string.Equals(r, pane, StringComparison.OrdinalIgnoreCase)); + + Assert.True(clash is null, + $"A page is routed at \"/{clash}\" and the deploy publishes the task pane at \"/{pane}/\". " + + "On the host the directory wins and the page is unreachable — visitors get the pane. " + + "Move the page's route; the pane's address is in a manifest people have installed."); + } + + [Fact] + public void The_manifest_points_at_the_directory_the_deploy_publishes() + { + // The other half of the same seam: the pane's URL lives in the manifest Word loads, and the + // deploy decides where the files land. If those two disagree the add-in 404s, which Word + // reports as "this add-in may not load properly" — an error a long way from its cause. + string manifest = File.ReadAllText( + Path.Combine(RepoRoot(), "src", "SignsOfAI.Word", "manifest.xml")); + + var source = Regex.Match(manifest, @"= 2, $"SourceLocation \"{path}\" has no subdirectory to compare."); + Assert.Equal(PaneDirectory(), parts[^2]); + } +}