363 lines
12 KiB
Go
363 lines
12 KiB
Go
package contribution
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
|
)
|
|
|
|
// TestRegister_AddsContributions registers sidebar, view, command, settings contributions
|
|
// for plugin "test.plugin" and verifies they appear via Views(), Commands(),
|
|
// SettingsPanels(), SidebarItems().
|
|
func TestRegister_AddsContributions(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
contribs := &plugin.Contributions{
|
|
Views: []plugin.ContributionView{
|
|
{ID: "test.view1", Title: "View 1", Component: "TestComponent"},
|
|
},
|
|
Commands: []plugin.ContributionCommand{
|
|
{ID: "test.cmd1", Title: "Command 1"},
|
|
},
|
|
SettingsPanels: []plugin.ContributionSettingsPanel{
|
|
{ID: "test.settings1", Title: "Settings 1", Component: "SettingsComponent"},
|
|
},
|
|
SidebarItems: []plugin.ContributionSidebarItem{
|
|
{ID: "test.sidebar1", Title: "Sidebar 1", Icon: "icon", View: "test.view1", Position: 1},
|
|
},
|
|
}
|
|
|
|
r.Register("test.plugin", contribs)
|
|
|
|
// Verify counts
|
|
if got := len(r.Views()); got != 1 {
|
|
t.Errorf("Views(): got %d, want 1", got)
|
|
}
|
|
if got := len(r.Commands()); got != 1 {
|
|
t.Errorf("Commands(): got %d, want 1", got)
|
|
}
|
|
if got := len(r.SettingsPanels()); got != 1 {
|
|
t.Errorf("SettingsPanels(): got %d, want 1", got)
|
|
}
|
|
if got := len(r.SidebarItems()); got != 1 {
|
|
t.Errorf("SidebarItems(): got %d, want 1", got)
|
|
}
|
|
|
|
// Verify the PluginID is set correctly
|
|
if r.Views()[0].PluginID != "test.plugin" {
|
|
t.Errorf("Views()[0].PluginID = %q, want %q", r.Views()[0].PluginID, "test.plugin")
|
|
}
|
|
if r.Commands()[0].PluginID != "test.plugin" {
|
|
t.Errorf("Commands()[0].PluginID = %q, want %q", r.Commands()[0].PluginID, "test.plugin")
|
|
}
|
|
if r.SettingsPanels()[0].PluginID != "test.plugin" {
|
|
t.Errorf("SettingsPanels()[0].PluginID = %q, want %q", r.SettingsPanels()[0].PluginID, "test.plugin")
|
|
}
|
|
if r.SidebarItems()[0].PluginID != "test.plugin" {
|
|
t.Errorf("SidebarItems()[0].PluginID = %q, want %q", r.SidebarItems()[0].PluginID, "test.plugin")
|
|
}
|
|
|
|
// Verify item data is preserved
|
|
if r.Views()[0].Item.Title != "View 1" {
|
|
t.Errorf("Views()[0].Item.Title = %q, want %q", r.Views()[0].Item.Title, "View 1")
|
|
}
|
|
if r.Commands()[0].Item.Title != "Command 1" {
|
|
t.Errorf("Commands()[0].Item.Title = %q, want %q", r.Commands()[0].Item.Title, "Command 1")
|
|
}
|
|
if r.SettingsPanels()[0].Item.Title != "Settings 1" {
|
|
t.Errorf("SettingsPanels()[0].Item.Title = %q, want %q", r.SettingsPanels()[0].Item.Title, "Settings 1")
|
|
}
|
|
if r.SidebarItems()[0].Item.Title != "Sidebar 1" {
|
|
t.Errorf("SidebarItems()[0].Item.Title = %q, want %q", r.SidebarItems()[0].Item.Title, "Sidebar 1")
|
|
}
|
|
}
|
|
|
|
// TestUnregister_RemovesOwnedContributions registers for two plugins, unregisters one,
|
|
// and verifies only that plugin's contributions are removed.
|
|
func TestUnregister_RemovesOwnedContributions(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
contribA := &plugin.Contributions{
|
|
Views: []plugin.ContributionView{
|
|
{ID: "a.view1", Title: "A View", Component: "A"},
|
|
},
|
|
Commands: []plugin.ContributionCommand{
|
|
{ID: "a.cmd1", Title: "A Command"},
|
|
},
|
|
SettingsPanels: []plugin.ContributionSettingsPanel{
|
|
{ID: "a.settings1", Title: "A Settings", Component: "A"},
|
|
},
|
|
SidebarItems: []plugin.ContributionSidebarItem{
|
|
{ID: "a.sidebar1", Title: "A Sidebar", View: "a.view1"},
|
|
},
|
|
}
|
|
|
|
contribB := &plugin.Contributions{
|
|
Views: []plugin.ContributionView{
|
|
{ID: "b.view1", Title: "B View", Component: "B"},
|
|
},
|
|
Commands: []plugin.ContributionCommand{
|
|
{ID: "b.cmd1", Title: "B Command"},
|
|
},
|
|
SettingsPanels: []plugin.ContributionSettingsPanel{
|
|
{ID: "b.settings1", Title: "B Settings", Component: "B"},
|
|
},
|
|
SidebarItems: []plugin.ContributionSidebarItem{
|
|
{ID: "b.sidebar1", Title: "B Sidebar", View: "b.view1"},
|
|
},
|
|
}
|
|
|
|
r.Register("plugin.a", contribA)
|
|
r.Register("plugin.b", contribB)
|
|
|
|
// Unregister plugin.a
|
|
r.Unregister("plugin.a")
|
|
|
|
// Verify plugin.a contributions are removed
|
|
if got := r.Views(); len(got) != 1 || got[0].PluginID != "plugin.b" {
|
|
t.Errorf("Views: got %d items (first PluginID=%q), want 1 from plugin.b", len(got), safePluginIDView(got))
|
|
}
|
|
if got := r.Commands(); len(got) != 1 || got[0].PluginID != "plugin.b" {
|
|
t.Errorf("Commands: got %d items (first PluginID=%q), want 1 from plugin.b", len(got), safePluginIDCmd(got))
|
|
}
|
|
if got := r.SettingsPanels(); len(got) != 1 || got[0].PluginID != "plugin.b" {
|
|
t.Errorf("SettingsPanels: got %d items (first PluginID=%q), want 1 from plugin.b", len(got), safePluginIDSettings(got))
|
|
}
|
|
if got := r.SidebarItems(); len(got) != 1 || got[0].PluginID != "plugin.b" {
|
|
t.Errorf("SidebarItems: got %d items (first PluginID=%q), want 1 from plugin.b", len(got), safePluginIDSidebar(got))
|
|
}
|
|
|
|
// Verify plugin.b data is intact
|
|
if r.Views()[0].Item.ID != "b.view1" {
|
|
t.Errorf("Remaining View ID: got %q, want %q", r.Views()[0].Item.ID, "b.view1")
|
|
}
|
|
if r.Commands()[0].Item.ID != "b.cmd1" {
|
|
t.Errorf("Remaining Command ID: got %q, want %q", r.Commands()[0].Item.ID, "b.cmd1")
|
|
}
|
|
if r.SettingsPanels()[0].Item.ID != "b.settings1" {
|
|
t.Errorf("Remaining SettingsPanel ID: got %q, want %q", r.SettingsPanels()[0].Item.ID, "b.settings1")
|
|
}
|
|
if r.SidebarItems()[0].Item.ID != "b.sidebar1" {
|
|
t.Errorf("Remaining SidebarItem ID: got %q, want %q", r.SidebarItems()[0].Item.ID, "b.sidebar1")
|
|
}
|
|
}
|
|
|
|
// safe helpers for error messages when slices are empty
|
|
func safePluginIDView(items []ContributionView) string {
|
|
if len(items) == 0 {
|
|
return "<empty>"
|
|
}
|
|
return items[0].PluginID
|
|
}
|
|
func safePluginIDCmd(items []ContributionCommand) string {
|
|
if len(items) == 0 {
|
|
return "<empty>"
|
|
}
|
|
return items[0].PluginID
|
|
}
|
|
func safePluginIDSettings(items []ContributionSettingsPanel) string {
|
|
if len(items) == 0 {
|
|
return "<empty>"
|
|
}
|
|
return items[0].PluginID
|
|
}
|
|
func safePluginIDSidebar(items []ContributionSidebarItem) string {
|
|
if len(items) == 0 {
|
|
return "<empty>"
|
|
}
|
|
return items[0].PluginID
|
|
}
|
|
|
|
// TestListByPoint registers various types and calls ListByPoint for each point type,
|
|
// verifying correct counts.
|
|
func TestListByPoint(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
contrib := &plugin.Contributions{
|
|
Views: []plugin.ContributionView{{ID: "v1", Title: "V1", Component: "C"}},
|
|
Commands: []plugin.ContributionCommand{{ID: "c1", Title: "C1"}},
|
|
SettingsPanels: []plugin.ContributionSettingsPanel{{ID: "s1", Title: "S1", Component: "C"}},
|
|
SidebarItems: []plugin.ContributionSidebarItem{{ID: "si1", Title: "SI1", View: "v1"}},
|
|
FileActions: []plugin.ContributionAction{{ID: "fa1", Label: "FA1"}},
|
|
NoteActions: []plugin.ContributionAction{{ID: "na1", Label: "NA1"}},
|
|
ContextMenuEntries: []plugin.ContributionContextMenuEntry{{ID: "cm1", Label: "CM1", Context: "file"}},
|
|
SearchProviders: []plugin.ContributionSearchProvider{{ID: "sp1", Label: "SP1", Handler: "h"}},
|
|
ActivityProviders: []plugin.ContributionActivityProvider{{ID: "ap1", Events: []string{"test"}, Handler: "h"}},
|
|
StatusBarItems: []plugin.ContributionStatusBarItem{{ID: "sb1", Label: "SB1"}},
|
|
OpenProviders: []plugin.ContributionOpenProvider{{
|
|
ID: "op1",
|
|
Title: "Open Provider 1",
|
|
Priority: 100,
|
|
Component: "OpenProvider",
|
|
Supports: []plugin.OpenProviderSupport{{
|
|
Kind: "vault-file",
|
|
Extensions: []string{".md"},
|
|
Contexts: []string{"generic-markdown", "notes-markdown"},
|
|
}},
|
|
}},
|
|
}
|
|
|
|
r.Register("test.plugin", contrib)
|
|
|
|
tests := []struct {
|
|
point ContributionPointType
|
|
want int
|
|
}{
|
|
{PointViews, 1},
|
|
{PointCommands, 1},
|
|
{PointSettingsPanels, 1},
|
|
{PointSidebarItems, 1},
|
|
{PointFileActions, 1},
|
|
{PointNoteActions, 1},
|
|
{PointContextMenus, 1},
|
|
{PointSearchProviders, 1},
|
|
{PointActivity, 1},
|
|
{PointStatusBar, 1},
|
|
{PointOpenProviders, 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := r.ListByPoint(tt.point)
|
|
if len(got) != tt.want {
|
|
t.Errorf("ListByPoint(%q): got %d items, want %d", tt.point, len(got), tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenProviders_RegisterReplaceUnregister(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
r.Register("editor.plugin", &plugin.Contributions{
|
|
OpenProviders: []plugin.ContributionOpenProvider{{
|
|
ID: "editor.markdown",
|
|
Title: "Markdown",
|
|
Priority: 50,
|
|
Component: "MarkdownEditor",
|
|
Supports: []plugin.OpenProviderSupport{{
|
|
Kind: "vault-file",
|
|
Extensions: []string{".md", ".markdown"},
|
|
Contexts: []string{"generic-markdown", "notes-markdown"},
|
|
}},
|
|
}},
|
|
})
|
|
|
|
providers := r.OpenProviders()
|
|
if len(providers) != 1 {
|
|
t.Fatalf("OpenProviders count = %d, want 1", len(providers))
|
|
}
|
|
if providers[0].PluginID != "editor.plugin" || providers[0].Item.Component != "MarkdownEditor" {
|
|
t.Fatalf("provider = %+v", providers[0])
|
|
}
|
|
|
|
r.Register("editor.plugin", &plugin.Contributions{
|
|
OpenProviders: []plugin.ContributionOpenProvider{{
|
|
ID: "editor.text",
|
|
Title: "Text",
|
|
Priority: 10,
|
|
Component: "TextEditor",
|
|
Supports: []plugin.OpenProviderSupport{{
|
|
Kind: "vault-file",
|
|
Extensions: []string{".txt"},
|
|
}},
|
|
}},
|
|
})
|
|
|
|
providers = r.OpenProviders()
|
|
if len(providers) != 1 || providers[0].Item.ID != "editor.text" {
|
|
t.Fatalf("providers after replace = %+v", providers)
|
|
}
|
|
|
|
r.Unregister("editor.plugin")
|
|
if got := len(r.OpenProviders()); got != 0 {
|
|
t.Fatalf("OpenProviders after unregister = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
// TestRegister_DuplicatePrevention calls Register twice for the same plugin
|
|
// (simulating reload) and checks contributions appear only once (no duplicates).
|
|
// This is the KEY TEST for idempotent re-registration.
|
|
func TestRegister_DuplicatePrevention(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
contrib := &plugin.Contributions{
|
|
Views: []plugin.ContributionView{
|
|
{ID: "test.view1", Title: "View 1", Component: "C"},
|
|
},
|
|
Commands: []plugin.ContributionCommand{
|
|
{ID: "test.cmd1", Title: "Cmd 1"},
|
|
},
|
|
SettingsPanels: []plugin.ContributionSettingsPanel{
|
|
{ID: "test.settings1", Title: "Settings 1", Component: "C"},
|
|
},
|
|
SidebarItems: []plugin.ContributionSidebarItem{
|
|
{ID: "test.sidebar1", Title: "Sidebar 1", View: "test.view1"},
|
|
},
|
|
}
|
|
|
|
// First registration
|
|
r.Register("test.plugin", contrib)
|
|
// Second registration — simulates plugin reload
|
|
r.Register("test.plugin", contrib)
|
|
|
|
// Each type should have only 1 entry (no duplicates)
|
|
if got := len(r.Views()); got != 1 {
|
|
t.Errorf("Views after double Register: got %d, want 1 (no duplicates)", got)
|
|
}
|
|
if got := len(r.Commands()); got != 1 {
|
|
t.Errorf("Commands after double Register: got %d, want 1 (no duplicates)", got)
|
|
}
|
|
if got := len(r.SettingsPanels()); got != 1 {
|
|
t.Errorf("SettingsPanels after double Register: got %d, want 1 (no duplicates)", got)
|
|
}
|
|
if got := len(r.SidebarItems()); got != 1 {
|
|
t.Errorf("SidebarItems after double Register: got %d, want 1 (no duplicates)", got)
|
|
}
|
|
|
|
// Also verify the item data is preserved correctly
|
|
if r.Views()[0].Item.ID != "test.view1" {
|
|
t.Errorf("View ID after reload: got %q, want %q", r.Views()[0].Item.ID, "test.view1")
|
|
}
|
|
if r.Commands()[0].Item.ID != "test.cmd1" {
|
|
t.Errorf("Command ID after reload: got %q, want %q", r.Commands()[0].Item.ID, "test.cmd1")
|
|
}
|
|
if r.SettingsPanels()[0].Item.ID != "test.settings1" {
|
|
t.Errorf("SettingsPanel ID after reload: got %q, want %q", r.SettingsPanels()[0].Item.ID, "test.settings1")
|
|
}
|
|
if r.SidebarItems()[0].Item.ID != "test.sidebar1" {
|
|
t.Errorf("SidebarItem ID after reload: got %q, want %q", r.SidebarItems()[0].Item.ID, "test.sidebar1")
|
|
}
|
|
}
|
|
|
|
// TestUnregister_NoSideEffects verifies that unregistering a non-existent plugin
|
|
// doesn't crash or corrupt the registry.
|
|
func TestUnregister_NoSideEffects(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
// Register a plugin
|
|
contrib := &plugin.Contributions{
|
|
Views: []plugin.ContributionView{
|
|
{ID: "v1", Title: "V1", Component: "C"},
|
|
},
|
|
}
|
|
r.Register("existing.plugin", contrib)
|
|
|
|
// Unregister a plugin that was never registered — should not panic
|
|
r.Unregister("nonexistent.plugin")
|
|
|
|
// Existing plugin's contributions should still be intact
|
|
if got := len(r.Views()); got != 1 {
|
|
t.Errorf("Views after unregistering non-existent: got %d, want 1", got)
|
|
}
|
|
if r.Views()[0].PluginID != "existing.plugin" {
|
|
t.Errorf("PluginID after unregistering non-existent: got %q, want %q", r.Views()[0].PluginID, "existing.plugin")
|
|
}
|
|
|
|
// Unregister with empty string — should not panic
|
|
r.Unregister("")
|
|
|
|
// Still intact
|
|
if got := len(r.Views()); got != 1 {
|
|
t.Errorf("Views after unregistering empty string: got %d, want 1", got)
|
|
}
|
|
}
|