diff --git a/ProjectDirector/ProjectDirector.cs b/ProjectDirector/ProjectDirector.cs
index 5c400ef..1b26be5 100644
--- a/ProjectDirector/ProjectDirector.cs
+++ b/ProjectDirector/ProjectDirector.cs
@@ -894,7 +894,12 @@ private void ShowCollapsiblePanel(string name, Action contentDelegate)
ImGuiProbes.MarkItem("panel", name);
if (expanded)
{
+ // CollapsingHeader opens no ID scope of its own, so without this two panels drawing a widget
+ // with the same label (each owner's ".github" row) would give both the same ID, and ImGui would
+ // hand a click on the second to the first.
+ ImGui.PushID(name);
contentDelegate?.Invoke();
+ ImGui.PopID();
open = true;
}
else
@@ -1020,7 +1025,7 @@ private void ShowRepos(GitHubOwnerName owner)
ImGui.SameLine();
bool isSelected = Options.BaseRepo == repoName;
bool clicked = ImGui.Selectable(gitHubRepo.RepoName, ref isSelected);
- ImGuiProbes.MarkItem("repo", gitHubRepo.RepoName);
+ ImGuiProbes.MarkItem("repo", repoName);
if (clicked)
{
SwitchPage(repoName);
diff --git a/tests/ProjectDirector.UITests/Gallery/GalleryApp.cs b/tests/ProjectDirector.UITests/Gallery/GalleryApp.cs
index b85c9d1..3540794 100644
--- a/tests/ProjectDirector.UITests/Gallery/GalleryApp.cs
+++ b/tests/ProjectDirector.UITests/Gallery/GalleryApp.cs
@@ -39,7 +39,13 @@ private GalleryApp(ImGuiAppHarness harness, ProjectDirectorOptions options, Cred
/// Starts the application at the given display size.
/// The size of the window the application draws into.
/// The running application.
- internal static GalleryApp Start((int Width, int Height) display)
+ internal static GalleryApp Start((int Width, int Height) display) => Start(display, GalleryRepositories.CreateOptions);
+
+ /// Starts the application at the given display size over settings the caller builds.
+ /// The size of the window the application draws into.
+ /// Builds the settings the application runs on, after the settings file and secret store are redirected.
+ /// The running application.
+ internal static GalleryApp Start((int Width, int Height) display, Func createOptions)
{
AppData.ConfigureForTesting(() => new MockFileSystem());
CredentialCache credentials = new(new InMemoryCredentialStore());
@@ -47,7 +53,7 @@ internal static GalleryApp Start((int Width, int Height) display)
try
{
- ProjectDirectorOptions options = GalleryRepositories.CreateOptions();
+ ProjectDirectorOptions options = createOptions();
ProjectDirector director = new(options);
ImGuiAppHarness harness = ImGuiAppHarness.Start(director.BuildConfig(), new HarnessOptions
{
@@ -68,7 +74,7 @@ internal static GalleryApp Start((int Width, int Height) display)
/// The repository's own name.
internal void SelectRepository(string repo)
{
- Harness.Click($"repo/{repo}");
+ Harness.Click($"repo/{GalleryRepositories.FullName(repo)}");
GitRepository selected = Options.Repos[GalleryRepositories.FullName(repo)];
// The comparison runs git on a background task, which publishes whenever it finishes rather
diff --git a/tests/ProjectDirector.UITests/RepoPanelTests.cs b/tests/ProjectDirector.UITests/RepoPanelTests.cs
new file mode 100644
index 0000000..0230a2a
--- /dev/null
+++ b/tests/ProjectDirector.UITests/RepoPanelTests.cs
@@ -0,0 +1,64 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.ProjectDirector.UITests;
+
+using ktsu.ProjectDirector.UITests.Gallery;
+using ktsu.Semantics.Paths;
+
+///
+/// Drives the left panel's repository list, where every owner's repositories are drawn as rows of
+/// the same kind of widget.
+///
+[TestClass]
+public sealed class RepoPanelTests
+{
+ private const string SharedName = ".github";
+
+ private static readonly string[] Owners = ["ktsu-dev", "matt-edmondson"];
+
+ [TestMethod]
+ public void ClickingASharedRepoName_SelectsThatOwnersRepository()
+ {
+ using GalleryApp app = GalleryApp.Start(AppGallery.Display, CreateOptions);
+ app.Harness.Step(2);
+
+ // Click the later owner first: it is the row an ID clash with the earlier owner's row swallows.
+ foreach (string owner in Owners.Reverse())
+ {
+ FullyQualifiedGitHubRepoName expected = FullName(owner);
+ app.Harness.Click($"repo/{expected}");
+ app.Harness.Step(2);
+
+ Assert.AreEqual(expected, app.Options.BaseRepo, $"Clicking {owner}'s {SharedName} did not select it.");
+ }
+ }
+
+ private static FullyQualifiedGitHubRepoName FullName(string owner) =>
+ FullyQualifiedGitHubRepoName.Create($"{owner}.{SharedName}");
+
+ private static ProjectDirectorOptions CreateOptions()
+ {
+ // Nothing is cloned, so the paths are never created; they only have to differ per owner.
+ string root = Path.Combine(Path.GetTempPath(), "projectdirector-repo-panel");
+ ProjectDirectorOptions options = new()
+ {
+ DevDirectory = AbsoluteDirectoryPath.Create(root),
+ };
+
+ foreach (string ownerName in Owners)
+ {
+ GitHubOwnerName owner = GitHubOwnerName.Create(ownerName);
+ _ = options.GitHubOwners.Add(owner);
+ options.Repos[FullName(ownerName)] = new GitHubRepository()
+ {
+ OwnerName = owner,
+ RepoName = GitHubRepoName.Create(SharedName),
+ LocalPath = FullyQualifiedLocalRepoPath.Create(Path.Combine(root, ownerName, SharedName)),
+ RemotePath = GitRemotePath.Create($"https://github.com/{ownerName}/{SharedName}"),
+ MinFetchIntervalSeconds = 0,
+ };
+ }
+
+ return options;
+ }
+}