diff --git a/AGENTS.md b/AGENTS.md index ea7af71..f21e0f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ Insert Multiple Attachments is an Obsidian plugin that allows inserting multiple - **Root config files** are thin re-exports — actual logic lives in `scripts/` (`eslint.config.mts` → `scripts/eslint-config.ts`, etc.). - **`src/`** — plugin source: - `main.ts` — Obsidian entry point; imports the SCSS and default-exports the `Plugin` class. - - `plugin.ts` — `Plugin extends PluginBase`; `onloadImpl` wires up the settings component, settings tab, menu-event registrar, and the `CommandHandlerComponent` holding the `InvokeCommandHandler`. + - `plugin.ts` — `Plugin extends PluginBase`; `onloadImpl` wires up the settings component and settings tab, then registers the `InvokeCommandHandler` via the base-provided `this.commandHandlerComponent` (the active-file provider, command registrar, and menu-event registrar are owned by `PluginBase` since obsidian-dev-utils 86.0.0). - `plugin-settings.ts` — `PluginSettings` data class: `attachmentLinksPrefix`, `attachmentLinksDelimiter` (default `\n\n`), `attachmentLinksSuffix`. - `plugin-settings-component.ts` — `PluginSettingsComponent extends PluginSettingsComponentBase`; registers a legacy-settings converter mapping the old `shouldInsertDoubleLinesBetweenAttachmentLinks` flag to a delimiter. - `plugin-settings-tab.ts` — `PluginSettingsTab extends PluginSettingsTabBase`; renders prefix/delimiter/suffix text settings with whitespace visualized as `␣`/`↵` via value converters. diff --git a/package-lock.json b/package-lock.json index 95a9c3a..611bbee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@commitlint/cli": "^21.2.1", "@commitlint/config-conventional": "^21.2.0", "@commitlint/types": "^21.2.0", - "@obsidian-typings/obsidian-public-latest": "^6.22.0", + "@obsidian-typings/obsidian-public-latest": "^6.23.0", "@total-typescript/ts-reset": "^0.6.1", "@tsconfig/strictest": "^2.0.8", "@types/node": "^26.1.1", @@ -25,8 +25,8 @@ "jsdom": "^29.1.1", "nano-staged": "^1.0.2", "obsidian": "^1.13.1", - "obsidian-dev-utils": "^85.1.0", - "obsidian-integration-testing": "^6.2.0", + "obsidian-dev-utils": "^86.0.0", + "obsidian-integration-testing": "^8.1.0", "obsidian-test-mocks": "^3.6.0", "vitest": "^4.1.10" } @@ -2560,9 +2560,9 @@ } }, "node_modules/@obsidian-typings/obsidian-public-1.12.7": { - "version": "6.24.0", - "resolved": "https://registry.npmjs.org/@obsidian-typings/obsidian-public-1.12.7/-/obsidian-public-1.12.7-6.24.0.tgz", - "integrity": "sha512-Ktlvsen9jVDhDgupZp2UYjs14eJw0ESZCu28+iZZ1lVN7KBnsGM7xnq/3bPNrVE3FO1UniFRkDwMFcyrH44M2g==", + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/@obsidian-typings/obsidian-public-1.12.7/-/obsidian-public-1.12.7-6.26.0.tgz", + "integrity": "sha512-/EQqzZlpmub7nY3St+gaLISgoqTBo2cBFgLZZOdPZEz/VRaSLCK03g8X4TzDRIZ85uEGdu8VHoCPUm97tRaFuA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2570,13 +2570,13 @@ } }, "node_modules/@obsidian-typings/obsidian-public-latest": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/@obsidian-typings/obsidian-public-latest/-/obsidian-public-latest-6.22.0.tgz", - "integrity": "sha512-n9Bfwu3e+j8iMsTrF3EXs3W5KwnnIkmLZldohd2DCKg6NbZP9UmQeCi56aVX+7UgJRDPa3erd1XopWLANhoHyw==", + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/@obsidian-typings/obsidian-public-latest/-/obsidian-public-latest-6.24.0.tgz", + "integrity": "sha512-tRKsKw/SYiRB/3ucQIE3cKk8pQJIignsRe65fmb+/XbHIVetiHnO8Ke8PEN9WsLiY3tlNzNU+WGksA578JGaMw==", "dev": true, "license": "MIT", "dependencies": { - "@obsidian-typings/obsidian-public-1.12.7": "^6.24.0" + "@obsidian-typings/obsidian-public-1.12.7": "^6.26.0" } }, "node_modules/@oxc-project/types": { @@ -5152,6 +5152,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adm-zip": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", + "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -12943,9 +12953,9 @@ } }, "node_modules/obsidian-dev-utils": { - "version": "85.1.0", - "resolved": "https://registry.npmjs.org/obsidian-dev-utils/-/obsidian-dev-utils-85.1.0.tgz", - "integrity": "sha512-n+4F6/PtS9w80Ifei3cDhbrY6t3DUOSdM18HyxHn0w7B+reV0BIMX/tt+VX5GMQqMKgf0IUNA8U+TxBPZIGyjA==", + "version": "86.1.3", + "resolved": "https://registry.npmjs.org/obsidian-dev-utils/-/obsidian-dev-utils-86.1.3.tgz", + "integrity": "sha512-XH08KgaQmMULwL7/dEWEkEDI0Y3FXdZmTXuZBNfBmgZLmjmUkVn98y3Hz3bkJQCuE6ic6NmGjb5odZgBze1zdg==", "dev": true, "license": "MIT", "dependencies": { @@ -12956,7 +12966,7 @@ "@eslint/config-helpers": "^0.6.0", "@eslint/js": "^10.0.1", "@lezer/common": "1.2.3", - "@obsidian-typings/obsidian-public-latest": "^6.22.0", + "@obsidian-typings/obsidian-public-latest": "^6.23.0", "@stylistic/eslint-plugin": "^5.10.0", "@tsconfig/strictest": "^2.0.8", "@types/babel__core": "^7.20.5", @@ -12973,6 +12983,7 @@ "@types/shell-quote": "^1.7.5", "@typescript-eslint/eslint-plugin": "^8.63.0", "@typescript-eslint/parser": "^8.63.0", + "adm-zip": "^0.5.18", "commander": "^15.0.0", "compare-versions": "^6.1.1", "cspell": "^10.0.1", @@ -13016,8 +13027,7 @@ "svelte-preprocess": "^6.0.5", "type-fest": "^5.8.0", "typescript": "6.0.3", - "typescript-eslint": "^8.63.0", - "unist-util-visit": "^5.1.0" + "typescript-eslint": "^8.63.0" }, "engines": { "node": ">=22.0.0" @@ -13096,9 +13106,9 @@ } }, "node_modules/obsidian-integration-testing": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/obsidian-integration-testing/-/obsidian-integration-testing-6.2.0.tgz", - "integrity": "sha512-NSocP9kPgMhlIyTk/nbVDkP6o2AcvnNgAhrrKpky6EMHstXITWSnJTYPYV7mAHayD3DBT7/28vdxmD1U/cVKxw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/obsidian-integration-testing/-/obsidian-integration-testing-8.2.0.tgz", + "integrity": "sha512-32RxOHXoEAFgkH9vtR80AhtPaxm3efIwMcASYWt+2DfLesDLgtUCmCpnQG38JA+HJoM0Y4+T0xZg+kLptQsiwQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 46e2ccc..812e12a 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@commitlint/cli": "^21.2.1", "@commitlint/config-conventional": "^21.2.0", "@commitlint/types": "^21.2.0", - "@obsidian-typings/obsidian-public-latest": "^6.22.0", + "@obsidian-typings/obsidian-public-latest": "^6.23.0", "@total-typescript/ts-reset": "^0.6.1", "@tsconfig/strictest": "^2.0.8", "@types/node": "^26.1.1", @@ -61,8 +61,8 @@ "jsdom": "^29.1.1", "nano-staged": "^1.0.2", "obsidian": "^1.13.1", - "obsidian-dev-utils": "^85.1.0", - "obsidian-integration-testing": "^6.2.0", + "obsidian-dev-utils": "^86.0.0", + "obsidian-integration-testing": "^8.1.0", "obsidian-test-mocks": "^3.6.0", "vitest": "^4.1.10" } diff --git a/src/plugin.test.ts b/src/plugin.test.ts index c7274f5..ca6900a 100644 --- a/src/plugin.test.ts +++ b/src/plugin.test.ts @@ -2,14 +2,13 @@ import type { App as AppOriginal, PluginManifest } from 'obsidian'; +import type { CommandHandlerComponent } from 'obsidian-dev-utils/obsidian/command-handlers/command-handler-component'; -import { AppActiveFileProvider } from 'obsidian-dev-utils/obsidian/active-file-provider'; -import { CommandHandlerComponent } from 'obsidian-dev-utils/obsidian/command-handlers/command-handler-component'; -import { PluginCommandRegistrar } from 'obsidian-dev-utils/obsidian/command-registrar'; -import { MenuEventRegistrarComponent } from 'obsidian-dev-utils/obsidian/components/menu-event-registrar-component'; +import { castTo } from 'obsidian-dev-utils/object-utils'; import { PluginSettingsTabComponent } from 'obsidian-dev-utils/obsidian/components/plugin-settings-tab-component'; import { PluginDataHandler } from 'obsidian-dev-utils/obsidian/data-handler'; import { PluginEventSourceImpl } from 'obsidian-dev-utils/obsidian/plugin/plugin-event-source'; +import { strictProxy } from 'obsidian-dev-utils/strict-proxy'; import { App } from 'obsidian-test-mocks/obsidian'; import { beforeEach, @@ -24,14 +23,6 @@ import { PluginSettingsComponent } from './plugin-settings-component.ts'; import { PluginSettingsTab } from './plugin-settings-tab.ts'; import { Plugin } from './plugin.ts'; -// The real `PluginBase.onload()` loads dev-utils' own notice/context/debug components, which read a -// Shared-state bag off the app via `getObsidianDevUtilsState`. The strict App mock has no such bag, so -// Stub this one utility (return a fresh value wrapper per call) — mirroring dev-utils' own PluginBase test. -vi.mock('obsidian-dev-utils/obsidian/app', async (importOriginal) => ({ - ...await importOriginal(), - getObsidianDevUtilsState: vi.fn((_app: unknown, _key: string, defaultValue: unknown) => ({ value: defaultValue })) -})); - // A dev-utils component added via `addChild` must be loadable, so its stub returns a real `Component`. The // Flowing instance is the stub's return value (`mock.results[0].value`), not the discarded `this`. interface ObsidianComponentModule { @@ -49,26 +40,10 @@ async function loadableComponentStub(): Promise> { }); } -vi.mock('obsidian-dev-utils/obsidian/components/menu-event-registrar-component', async () => ({ - MenuEventRegistrarComponent: await loadableComponentStub() -})); - vi.mock('obsidian-dev-utils/obsidian/components/plugin-settings-tab-component', async () => ({ PluginSettingsTabComponent: await loadableComponentStub() })); -vi.mock('obsidian-dev-utils/obsidian/command-handlers/command-handler-component', async () => ({ - CommandHandlerComponent: await loadableComponentStub() -})); - -vi.mock('obsidian-dev-utils/obsidian/active-file-provider', () => ({ - AppActiveFileProvider: vi.fn() -})); - -vi.mock('obsidian-dev-utils/obsidian/command-registrar', () => ({ - PluginCommandRegistrar: vi.fn() -})); - vi.mock('obsidian-dev-utils/obsidian/data-handler', () => ({ PluginDataHandler: vi.fn() })); @@ -96,10 +71,6 @@ vi.mock('./plugin-settings-component.ts', () => ({ }) })); -const MockAppActiveFileProvider = vi.mocked(AppActiveFileProvider); -const MockCommandHandlerComponent = vi.mocked(CommandHandlerComponent); -const MockPluginCommandRegistrar = vi.mocked(PluginCommandRegistrar); -const MockMenuEventRegistrarComponent = vi.mocked(MenuEventRegistrarComponent); const MockPluginSettingsTabComponent = vi.mocked(PluginSettingsTabComponent); const MockPluginDataHandler = vi.mocked(PluginDataHandler); const MockPluginEventSourceImpl = vi.mocked(PluginEventSourceImpl); @@ -116,30 +87,42 @@ const manifest: PluginManifest = { version: '1.0.0' }; +// ODU 86.0.0 moved the command-handler component into `PluginBase` (`this.commandHandlerComponent`). Driving +// `onloadImpl()` directly with a seeded `_commandHandlerComponent` keeps this a focused wiring test — the base +// `onload()` (notice/context/debug components) is dev-utils' own concern, covered by its tests. +interface PluginInternals { + _commandHandlerComponent: CommandHandlerComponent; + onloadImpl(): void; +} + let app: AppOriginal; function instanceOf(mock: ReturnType): unknown { return mock.mock.results[0]?.value; } +function seedAndRun(plugin: Plugin): ReturnType { + const internals = castTo(plugin); + const registerCommandHandlers = vi.fn(); + internals._commandHandlerComponent = strictProxy({ registerCommandHandlers }); + internals.onloadImpl(); + return registerCommandHandlers; +} + describe('Plugin', () => { beforeEach(() => { vi.clearAllMocks(); - const appMock = App.createConfigured__(); - appMock.workspace.onLayoutReady = vi.fn((cb: () => void) => { - cb(); - }); - app = appMock.asOriginalType__(); + app = App.createConfigured__().asOriginalType__(); }); it('should create a plugin instance', () => { expect(new Plugin(app, manifest)).toBeInstanceOf(Plugin); }); - describe('onload', () => { - it('should create PluginSettingsComponent with the data handler and plugin event source', async () => { + describe('onloadImpl', () => { + it('should create PluginSettingsComponent with the data handler and plugin event source', () => { const plugin = new Plugin(app, manifest); - await plugin.onload(); + seedAndRun(plugin); expect(MockPluginSettingsComponent).toHaveBeenCalledWith({ dataHandler: MockPluginDataHandler.mock.instances[0], @@ -147,9 +130,9 @@ describe('Plugin', () => { }); }); - it('should create PluginSettingsTab with the plugin and settings component', async () => { + it('should create PluginSettingsTab with the plugin and settings component', () => { const plugin = new Plugin(app, manifest); - await plugin.onload(); + seedAndRun(plugin); expect(MockPluginSettingsTab).toHaveBeenCalledWith({ plugin, @@ -157,9 +140,9 @@ describe('Plugin', () => { }); }); - it('should create PluginSettingsTabComponent with the plugin and settings tab', async () => { + it('should create PluginSettingsTabComponent with the plugin and settings tab', () => { const plugin = new Plugin(app, manifest); - await plugin.onload(); + seedAndRun(plugin); expect(MockPluginSettingsTabComponent).toHaveBeenCalledWith({ plugin, @@ -167,44 +150,33 @@ describe('Plugin', () => { }); }); - it('should create MenuEventRegistrarComponent with the app', async () => { + it('should create InvokeCommandHandler with the app and settings component', () => { const plugin = new Plugin(app, manifest); - await plugin.onload(); - - expect(MockMenuEventRegistrarComponent).toHaveBeenCalledWith(app); - }); - - it('should create InvokeCommandHandler with the app and settings component', async () => { - const plugin = new Plugin(app, manifest); - await plugin.onload(); + seedAndRun(plugin); const params = MockInvokeCommandHandler.mock.calls[0]?.[0]; expect(params?.app).toBe(app); expect(params?.pluginSettingsComponent).toBe(instanceOf(MockPluginSettingsComponent)); }); - it('should create CommandHandlerComponent wiring the invoke command and menu registrar', async () => { + it('should register the invoke command handler on the base command-handler component', () => { const plugin = new Plugin(app, manifest); - await plugin.onload(); + const registerCommandHandlers = seedAndRun(plugin); - expect(MockCommandHandlerComponent).toHaveBeenCalledWith({ - activeFileProvider: MockAppActiveFileProvider.mock.instances[0], - commandHandlers: [MockInvokeCommandHandler.mock.instances[0]], - commandRegistrar: MockPluginCommandRegistrar.mock.instances[0], - menuEventRegistrar: instanceOf(MockMenuEventRegistrarComponent), - pluginName: manifest.name - }); + expect(registerCommandHandlers).toHaveBeenCalledOnce(); + expect(registerCommandHandlers).toHaveBeenCalledWith([instanceOf(MockInvokeCommandHandler)]); }); - it('should add the four plugin components as children', async () => { + it('should add the two plugin components as children', () => { const plugin = new Plugin(app, manifest); const addChildSpy = vi.spyOn(plugin, 'addChild'); - await plugin.onload(); + seedAndRun(plugin); expect(addChildSpy).toHaveBeenCalledWith(instanceOf(MockPluginSettingsComponent)); expect(addChildSpy).toHaveBeenCalledWith(instanceOf(MockPluginSettingsTabComponent)); - expect(addChildSpy).toHaveBeenCalledWith(instanceOf(MockMenuEventRegistrarComponent)); - expect(addChildSpy).toHaveBeenCalledWith(instanceOf(MockCommandHandlerComponent)); + + const EXPECTED_ADD_CHILD_CALLS = 2; + expect(addChildSpy).toHaveBeenCalledTimes(EXPECTED_ADD_CHILD_CALLS); }); }); }); diff --git a/src/plugin.ts b/src/plugin.ts index da6a962..7bfa33d 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,7 +1,3 @@ -import { AppActiveFileProvider } from 'obsidian-dev-utils/obsidian/active-file-provider'; -import { CommandHandlerComponent } from 'obsidian-dev-utils/obsidian/command-handlers/command-handler-component'; -import { PluginCommandRegistrar } from 'obsidian-dev-utils/obsidian/command-registrar'; -import { MenuEventRegistrarComponent } from 'obsidian-dev-utils/obsidian/components/menu-event-registrar-component'; import { PluginSettingsTabComponent } from 'obsidian-dev-utils/obsidian/components/plugin-settings-tab-component'; import { PluginDataHandler } from 'obsidian-dev-utils/obsidian/data-handler'; import { PluginBase } from 'obsidian-dev-utils/obsidian/plugin/plugin'; @@ -28,20 +24,11 @@ export class Plugin extends PluginBase { }) }) ); - const menuEventRegistrar = this.addChild(new MenuEventRegistrarComponent(this.app)); - this.addChild( - new CommandHandlerComponent({ - activeFileProvider: new AppActiveFileProvider(this.app), - commandHandlers: [ - new InvokeCommandHandler({ - app: this.app, - pluginSettingsComponent - }) - ], - commandRegistrar: new PluginCommandRegistrar(this), - menuEventRegistrar, - pluginName: this.manifest.name + this.commandHandlerComponent.registerCommandHandlers([ + new InvokeCommandHandler({ + app: this.app, + pluginSettingsComponent }) - ); + ]); } }