Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/extension/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ export const activate = (context: vscode.ExtensionContext) => {
statusBarCreator,
store: appStore,
});
const treeProvider = CommandTreeProvider.create({ configManager, configReader, eventBus });
const treeProvider = CommandTreeProvider.create({ store: appStore });
const importExportManager = ImportExportManager.create({
configManager,
configReader,
Expand All @@ -346,8 +346,6 @@ export const activate = (context: vscode.ExtensionContext) => {
eventBus,
});

treeProvider.setButtonSetManager(buttonSetManager);

const webviewProvider = new ConfigWebviewProvider(
context.extensionUri,
configReader,
Expand Down
268 changes: 124 additions & 144 deletions src/internal/providers/command-tree-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ButtonConfig } from "../../pkg/types";
import { EventBus } from "../event-bus";
import { createAppStore, AppStoreInstance } from "../stores";
import { createTreeItems } from "../utils/ui-items";
import { CommandTreeItem, CommandTreeProvider, GroupTreeItem } from "./command-tree-provider";

Expand Down Expand Up @@ -261,204 +261,185 @@ describe("command-tree-provider", () => {
});

describe("CommandTreeProvider", () => {
const mockConfigReader = jest.fn();
const mockConfigManager = {
getButtonsWithFallback: jest.fn(),
};
let mockStore: AppStoreInstance;
let provider: CommandTreeProvider;

beforeEach(() => {
jest.clearAllMocks();
mockStore = createAppStore();
mockStore.getState().setButtons([
{ command: "echo test1", id: "btn-1", name: "Test Button 1" },
{ command: "echo test2", id: "btn-2", name: "Test Button 2" },
]);
});

describe("constructor", () => {
it("should create provider without EventBus", () => {
const provider = new CommandTreeProvider(mockConfigReader as any, mockConfigManager as any);
afterEach(() => {
if (provider) {
provider.dispose();
}
});

describe("static create", () => {
it("should create provider without store (uses global store)", () => {
provider = CommandTreeProvider.create();

expect(provider).toBeInstanceOf(CommandTreeProvider);
});

it("should create provider with EventBus and subscribe to events", () => {
const eventBus = new EventBus();
const onSpy = jest.spyOn(eventBus, "on");

const provider = new CommandTreeProvider(
mockConfigReader as any,
mockConfigManager as any,
eventBus
);
it("should create provider with custom store", () => {
provider = CommandTreeProvider.create({ store: mockStore });

expect(provider).toBeInstanceOf(CommandTreeProvider);
expect(onSpy).toHaveBeenCalledWith("buttonSet:created", expect.any(Function));
expect(onSpy).toHaveBeenCalledWith("buttonSet:deleted", expect.any(Function));
expect(onSpy).toHaveBeenCalledWith("buttonSet:renamed", expect.any(Function));
expect(onSpy).toHaveBeenCalledWith("buttonSet:switched", expect.any(Function));
expect(onSpy).toHaveBeenCalledWith("config:changed", expect.any(Function));
expect(onSpy).toHaveBeenCalledTimes(5);

provider.dispose();
eventBus.dispose();
});
});

describe("static create", () => {
it("should create provider without EventBus", () => {
const provider = CommandTreeProvider.create({
configManager: mockConfigManager as any,
configReader: mockConfigReader as any,
});
describe("Store subscription", () => {
it("should subscribe to store on creation", () => {
const subscribeSpy = jest.spyOn(mockStore, "subscribe");

expect(provider).toBeInstanceOf(CommandTreeProvider);
provider.dispose();
provider = CommandTreeProvider.create({ store: mockStore });

expect(subscribeSpy).toHaveBeenCalled();
});

it("should create provider with EventBus", () => {
const eventBus = new EventBus();
const provider = CommandTreeProvider.create({
configManager: mockConfigManager as any,
configReader: mockConfigReader as any,
eventBus,
});
it("should call refresh when store buttons change", () => {
provider = CommandTreeProvider.create({ store: mockStore });

expect(provider).toBeInstanceOf(CommandTreeProvider);
const refreshSpy = jest.spyOn(provider, "refresh");

provider.dispose();
eventBus.dispose();
});
});
mockStore
.getState()
.setButtons([{ command: "echo new", id: "new-btn", name: "New Button" }]);

describe("event subscription", () => {
it("should refresh when config:changed event is emitted", () => {
const eventBus = new EventBus();
const provider = new CommandTreeProvider(
mockConfigReader as any,
mockConfigManager as any,
eventBus
);
expect(refreshSpy).toHaveBeenCalled();
});

const refreshSpy = jest.spyOn(provider, "refresh");
it("should use buttons from store for tree items", async () => {
mockStore
.getState()
.setButtons([{ command: "echo store", id: "store-btn", name: "Store Button" }]);

eventBus.emit("config:changed", { scope: "workspace" });
provider = CommandTreeProvider.create({ store: mockStore });

expect(refreshSpy).toHaveBeenCalledTimes(1);
const items = await provider.getChildren();

provider.dispose();
eventBus.dispose();
expect(items).toHaveLength(1);
expect(items[0].label).toBe("Store Button");
expect((items[0] as CommandTreeItem).commandString).toBe("echo store");
});

it("should refresh when buttonSet:switched event is emitted", () => {
const eventBus = new EventBus();
const provider = new CommandTreeProvider(
mockConfigReader as any,
mockConfigManager as any,
eventBus
);
it("should reflect store changes in tree items", async () => {
provider = CommandTreeProvider.create({ store: mockStore });

const refreshSpy = jest.spyOn(provider, "refresh");

eventBus.emit("buttonSet:switched", { setName: "test-set" });
// Initial state
let items = await provider.getChildren();
expect(items).toHaveLength(2);

expect(refreshSpy).toHaveBeenCalledTimes(1);
// Update store
mockStore
.getState()
.setButtons([{ command: "echo updated", id: "updated-btn", name: "Updated Button" }]);

provider.dispose();
eventBus.dispose();
// Check updated items
items = await provider.getChildren();
expect(items).toHaveLength(1);
expect(items[0].label).toBe("Updated Button");
});
});

it("should refresh multiple times for multiple events", () => {
const eventBus = new EventBus();
const provider = new CommandTreeProvider(
mockConfigReader as any,
mockConfigManager as any,
eventBus
);

const refreshSpy = jest.spyOn(provider, "refresh");
describe("getChildren", () => {
it("should return root items when no element provided", async () => {
provider = CommandTreeProvider.create({ store: mockStore });

eventBus.emit("config:changed", { scope: "workspace" });
eventBus.emit("buttonSet:switched", { setName: "set1" });
eventBus.emit("config:changed", { scope: "global" });
eventBus.emit("buttonSet:switched", { setName: "set2" });
const items = await provider.getChildren();

expect(refreshSpy).toHaveBeenCalledTimes(4);
expect(items).toHaveLength(2);
expect(items[0]).toBeInstanceOf(CommandTreeItem);
expect(items[1]).toBeInstanceOf(CommandTreeItem);
});

provider.dispose();
eventBus.dispose();
it("should return group children for GroupTreeItem", async () => {
mockStore.getState().setButtons([
{
group: [
{ command: "echo sub1", id: "sub-1", name: "Sub 1" },
{ command: "echo sub2", id: "sub-2", name: "Sub 2" },
],
id: "group-1",
name: "Test Group",
},
]);

provider = CommandTreeProvider.create({ store: mockStore });

const rootItems = await provider.getChildren();
expect(rootItems).toHaveLength(1);
expect(rootItems[0]).toBeInstanceOf(GroupTreeItem);

const groupChildren = await provider.getChildren(rootItems[0] as GroupTreeItem);
expect(groupChildren).toHaveLength(2);
expect(groupChildren[0].label).toBe("Sub 1");
expect(groupChildren[1].label).toBe("Sub 2");
});

it("should not refresh after dispose", () => {
const eventBus = new EventBus();
const provider = new CommandTreeProvider(
mockConfigReader as any,
mockConfigManager as any,
eventBus
);
it("should return empty array for CommandTreeItem", async () => {
provider = CommandTreeProvider.create({ store: mockStore });

const refreshSpy = jest.spyOn(provider, "refresh");
const rootItems = await provider.getChildren();
const commandItem = rootItems[0] as CommandTreeItem;

provider.dispose();
const children = await provider.getChildren(commandItem);
expect(children).toEqual([]);
});

eventBus.emit("config:changed", { scope: "workspace" });
eventBus.emit("buttonSet:switched", { setName: "test-set" });
it("should handle empty buttons", async () => {
mockStore.getState().setButtons([]);

expect(refreshSpy).not.toHaveBeenCalled();
provider = CommandTreeProvider.create({ store: mockStore });

eventBus.dispose();
const items = await provider.getChildren();
expect(items).toHaveLength(0);
});
});

it("should not subscribe to events when EventBus is not provided", () => {
const provider = new CommandTreeProvider(mockConfigReader as any, mockConfigManager as any);

const refreshSpy = jest.spyOn(provider, "refresh");
describe("getTreeItem", () => {
it("should return the element itself", () => {
provider = CommandTreeProvider.create({ store: mockStore });

// No events to emit since no EventBus, just verify no crash
expect(refreshSpy).not.toHaveBeenCalled();
const item = new CommandTreeItem("Test", "echo test", false, undefined, "Test");
const result = provider.getTreeItem(item);

provider.dispose();
expect(result).toBe(item);
});
});

describe("dispose", () => {
it("should unsubscribe all event handlers", () => {
const eventBus = new EventBus();
const provider = new CommandTreeProvider(
mockConfigReader as any,
mockConfigManager as any,
eventBus
);

provider.dispose();
describe("refresh", () => {
it("should fire onDidChangeTreeData event", () => {
provider = CommandTreeProvider.create({ store: mockStore });

// Verify unsubscribe was called by checking that events no longer trigger refresh
const refreshSpy = jest.spyOn(provider, "refresh");
eventBus.emit("config:changed", { scope: "workspace" });
// Access private EventEmitter for testing
const emitter = (provider as any)._onDidChangeTreeData;
const fireSpy = jest.spyOn(emitter, "fire");

expect(refreshSpy).not.toHaveBeenCalled();
provider.refresh();

eventBus.dispose();
expect(fireSpy).toHaveBeenCalled();
});
});

it("should clear unsubscribers array", () => {
const eventBus = new EventBus();
const provider = new CommandTreeProvider(
mockConfigReader as any,
mockConfigManager as any,
eventBus
);
describe("dispose", () => {
it("should unsubscribe from store when disposed", () => {
provider = CommandTreeProvider.create({ store: mockStore });

provider.dispose();

// Dispose again should not throw
expect(() => provider.dispose()).not.toThrow();
const refreshSpy = jest.spyOn(provider, "refresh");
mockStore.getState().setButtons([]);

eventBus.dispose();
expect(refreshSpy).not.toHaveBeenCalled();
});

it("should dispose EventEmitter", () => {
const eventBus = new EventBus();
const provider = new CommandTreeProvider(
mockConfigReader as any,
mockConfigManager as any,
eventBus
);
provider = CommandTreeProvider.create({ store: mockStore });

// Access private field for testing
const emitter = (provider as any)._onDidChangeTreeData;
Expand All @@ -467,13 +448,12 @@ describe("command-tree-provider", () => {
provider.dispose();

expect(disposeSpy).toHaveBeenCalledTimes(1);

eventBus.dispose();
});

it("should work without EventBus", () => {
const provider = new CommandTreeProvider(mockConfigReader as any, mockConfigManager as any);
it("should not throw when disposed multiple times", () => {
provider = CommandTreeProvider.create({ store: mockStore });

provider.dispose();
expect(() => provider.dispose()).not.toThrow();
});
});
Expand Down
Loading
Loading