diff --git a/package.json b/package.json index 0106cce..c1c1874 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "scripts": { "dev": "vite build --watch --mode development", "build": "bun run tsc && vite build --mode production", - "typecheck": "tsc -noEmit -skipLibCheck", - "test": "bun test", - "test:log": "LOG_TESTS=true bun test", + "typecheck": "tsc -p tsconfig.json -noEmit -skipLibCheck && tsc -p tests/tsconfig.json -noEmit -skipLibCheck", + "test": "bun test --preload ./tests/setup.ts", + "test:log": "LOG_TESTS=true bun test --preload ./tests/setup.ts", "format": "prettier --write .", "format:check": "prettier --check .", "lint": "eslint --max-warnings=0 --no-warn-ignored packages/obsidian/src/**", diff --git a/packages/obsidian/src/main.ts b/packages/obsidian/src/main.ts index 0708db4..9d3be8b 100644 --- a/packages/obsidian/src/main.ts +++ b/packages/obsidian/src/main.ts @@ -194,6 +194,9 @@ export default class MediaDbPlugin extends Plugin { ); loadedSettings.propertyMappingModels = migratedModels.map(m => m.toJSON()); this.settings = loadedSettings; + this.mediaTypeManager.updateTemplates(this.settings); + this.mediaTypeManager.updateFolders(this.settings); + this.dateFormatter.setFormat(this.settings.customDateFormat); const legacyEntries = this.getLegacyApiKeyEntries(diskSettings); if (legacyEntries.length > 0) { diff --git a/packages/obsidian/src/utils/MediaTypeManager.ts b/packages/obsidian/src/utils/MediaTypeManager.ts index f3ef631..a0f4b60 100644 --- a/packages/obsidian/src/utils/MediaTypeManager.ts +++ b/packages/obsidian/src/utils/MediaTypeManager.ts @@ -79,7 +79,8 @@ export class MediaTypeManager { getFileName(mediaTypeModel: MediaTypeModel): string { // Ignore undefined tags since some search APIs do not return all properties in the model and produce clean file names even if errors occur - const fileName = replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType())!, mediaTypeModel, true); + const template = this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType()) ?? '{{ title }}'; + const fileName = replaceTags(template, mediaTypeModel, true); return this.cleanFileName(fileName); } diff --git a/tests/media-type-manager.test.ts b/tests/media-type-manager.test.ts new file mode 100644 index 0000000..fb03746 --- /dev/null +++ b/tests/media-type-manager.test.ts @@ -0,0 +1,62 @@ +import { expect, test } from 'bun:test'; +import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; +import type { MediaDbPluginSettings } from 'packages/obsidian/src/settings/Settings'; +import { MediaTypeManager } from 'packages/obsidian/src/utils/MediaTypeManager'; + +function createSettings(overrides: Partial = {}): MediaDbPluginSettings { + return { + movieFileNameTemplate: '{{ title }} ({{ year }})', + seriesFileNameTemplate: '', + seasonFileNameTemplate: '', + mangaFileNameTemplate: '', + gameFileNameTemplate: '', + wikiFileNameTemplate: '', + musicReleaseFileNameTemplate: '', + boardgameFileNameTemplate: '', + bookFileNameTemplate: '', + movieTemplate: '', + seriesTemplate: '', + seasonTemplate: '', + mangaTemplate: '', + gameTemplate: '', + wikiTemplate: '', + musicReleaseTemplate: '', + boardgameTemplate: '', + bookTemplate: '', + ...overrides, + } as MediaDbPluginSettings; +} + +test('getFileName falls back to title when templates have not been initialized', () => { + const manager = new MediaTypeManager(); + const movie = new MovieModel({ title: 'Arrival', year: '2016' }); + + expect(manager.getFileName(movie)).toBe('Arrival'); +}); + +test('getFileName uses the configured template for the media type', () => { + const manager = new MediaTypeManager(); + const movie = new MovieModel({ title: 'Arrival', year: '2016' }); + + manager.updateTemplates(createSettings({ movieFileNameTemplate: '{{ title }} - {{ year }}' })); + + expect(manager.getFileName(movie)).toBe('Arrival - 2016'); +}); + +test('getFileName omits undefined template values', () => { + const manager = new MediaTypeManager(); + const movie = new MovieModel({ title: 'Arrival', year: undefined }); + + manager.updateTemplates(createSettings({ movieFileNameTemplate: '{{ title }} {{ missingField }} {{ year }}' })); + + expect(manager.getFileName(movie)).toBe('Arrival '); +}); + +test('getFileName sanitizes illegal filename characters and duplicate spaces', () => { + const manager = new MediaTypeManager(); + const movie = new MovieModel({ title: 'Star: Trek / First ', year: '1996' }); + + manager.updateTemplates(createSettings({ movieFileNameTemplate: '{{ title }} {{ year }}' })); + + expect(manager.getFileName(movie)).toBe('Star - Trek - First Contact 1996'); +}); diff --git a/test/modal-helper.test.ts b/tests/modal-helper.test.ts similarity index 59% rename from test/modal-helper.test.ts rename to tests/modal-helper.test.ts index 02d8f98..e37d813 100644 --- a/test/modal-helper.test.ts +++ b/tests/modal-helper.test.ts @@ -1,46 +1,10 @@ -import { expect, mock, test } from 'bun:test'; +import { expect, test } from 'bun:test'; import type MediaDbPlugin from 'packages/obsidian/src/main'; -import type { ModalLifecycle, ModalSession } from 'packages/obsidian/src/utils/ModalHelper'; +import { ModalHelper, type ModalLifecycle, type ModalSession } from 'packages/obsidian/src/utils/ModalHelper'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { OutcomeStatus } from 'packages/obsidian/src/utils/result'; -mock.module('obsidian', () => ({ - AbstractInputSuggest: class {}, - Component: class { - load(): void {} - unload(): void {} - }, - DropdownComponent: class {}, - MarkdownRenderer: { render: async (): Promise => {} }, - MarkdownView: class {}, - Modal: class { - app: unknown; - - constructor(app: unknown) { - this.app = app; - } - - open(): void {} - close(): void {} - }, - Notice: class {}, - normalizePath: (path: string): string => path, - moment: Object.assign((value?: unknown): unknown => value, { locale: (): void => {} }), - parseYaml: (): unknown => ({}), - Plugin: class {}, - PluginSettingTab: class {}, - requestUrl: async (): Promise => ({}), - SecretComponent: class {}, - Setting: class {}, - SettingGroup: class {}, - stringifyYaml: (value: unknown): string => String(value), - TFile: class {}, - TFolder: class {}, - TextComponent: class {}, - ToggleComponent: class {}, -})); - class FakeModal implements ModalLifecycle { closeCount = 0; @@ -64,13 +28,12 @@ function createModalSession(cancelled: () => boolean): ModalSession { - const { ModalHelper } = await import('packages/obsidian/src/utils/ModalHelper'); +function createHelper(): ModalHelper { return new ModalHelper({} as MediaDbPlugin); } test('runModalTask closes the modal after active task success', async () => { - const helper = await createHelper(); + const helper = createHelper(); const session = createModalSession(() => false); const outcome = await helper.runModalTask(session, async () => 'done'); @@ -80,7 +43,7 @@ test('runModalTask closes the modal after active task success', async () => { }); test('runModalTask ignores late success after cancellation', async () => { - const helper = await createHelper(); + const helper = createHelper(); let cancelled = false; const session = createModalSession(() => cancelled); @@ -94,7 +57,7 @@ test('runModalTask ignores late success after cancellation', async () => { }); test('runModalTask ignores late errors after cancellation', async () => { - const helper = await createHelper(); + const helper = createHelper(); let cancelled = false; const session = createModalSession(() => cancelled); const fallback: MDBError = { kind: MDBErrorKind.Modal, message: 'Task failed' }; diff --git a/test/placeholder.test.ts b/tests/placeholder.test.ts similarity index 100% rename from test/placeholder.test.ts rename to tests/placeholder.test.ts diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..a2a5c16 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,37 @@ +import { mock } from 'bun:test'; + +mock.module('obsidian', () => ({ + AbstractInputSuggest: class {}, + Component: class { + load(): void {} + unload(): void {} + }, + DropdownComponent: class {}, + MarkdownRenderer: { render: async (): Promise => {} }, + MarkdownView: class {}, + Modal: class { + app: unknown; + + constructor(app: unknown) { + this.app = app; + } + + open(): void {} + close(): void {} + }, + Notice: class {}, + normalizePath: (path: string): string => path, + moment: Object.assign((value?: unknown): unknown => value, { locale: (): void => {} }), + parseYaml: (): unknown => ({}), + Plugin: class {}, + PluginSettingTab: class {}, + requestUrl: async (): Promise => ({}), + SecretComponent: class {}, + Setting: class {}, + SettingGroup: class {}, + stringifyYaml: (value: unknown): string => String(value), + TFile: class {}, + TFolder: class {}, + TextComponent: class {}, + ToggleComponent: class {}, +})); diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..bfa58b7 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "paths": { + "packages/*": ["../packages/*"], + "tests/*": ["./*"] + }, + "types": ["vite/client", "bun-types"] + }, + "include": ["**/*.ts"] +}