diff --git a/tests/engine/ImportAnalysisEngine.test.ts b/tests/engine/ImportAnalysisEngine.test.ts
index b6dad39..a8f0ed8 100644
--- a/tests/engine/ImportAnalysisEngine.test.ts
+++ b/tests/engine/ImportAnalysisEngine.test.ts
@@ -762,6 +762,97 @@ describe('ImportAnalysisEngine', () => {
expect(youtubeMacro?.usages[0].params.id).toBe('wordpress');
});
});
+
+ describe('HTML to Markdown Conversion - Linked Images', () => {
+ it('should convert linked images with image href (WordPress full-size pattern)', async () => {
+ setupDbReturns([], [], []);
+
+ const wxrData = createWxrData({
+ posts: [createWxrPost({
+ content: '
',
+ })],
+ });
+
+ const report = await engine.analyzeWxr(wxrData, '/test.xml');
+
+ // Should use the href URL (the full-size image) in markdown
+ expect(report.posts.items[0].markdownPreview).toContain('');
+ });
+
+ it('should convert linked images with non-image href (use img src)', async () => {
+ setupDbReturns([], [], []);
+
+ const wxrData = createWxrData({
+ posts: [createWxrPost({
+ content: '
',
+ })],
+ });
+
+ const report = await engine.analyzeWxr(wxrData, '/test.xml');
+
+ // Should use the img src since href is not an image
+ expect(report.posts.items[0].markdownPreview).toContain('');
+ });
+
+ it('should use img title as alt text when alt is empty', async () => {
+ setupDbReturns([], [], []);
+
+ const wxrData = createWxrData({
+ posts: [createWxrPost({
+ content: '
',
+ })],
+ });
+
+ const report = await engine.analyzeWxr(wxrData, '/test.xml');
+
+ // Should use title as alt text and include title in markdown
+ expect(report.posts.items[0].markdownPreview).toContain('');
+ });
+
+ it('should extract filename as alt text when both alt and title are empty', async () => {
+ setupDbReturns([], [], []);
+
+ const wxrData = createWxrData({
+ posts: [createWxrPost({
+ content: '
',
+ })],
+ });
+
+ const report = await engine.analyzeWxr(wxrData, '/test.xml');
+
+ // Should extract filename from URL as alt text
+ expect(report.posts.items[0].markdownPreview).toContain('beautiful-sunset.jpg');
+ });
+
+ it('should handle empty/whitespace content gracefully', async () => {
+ setupDbReturns([], [], []);
+
+ const wxrData = createWxrData({
+ posts: [createWxrPost({
+ content: ' ',
+ })],
+ });
+
+ const report = await engine.analyzeWxr(wxrData, '/test.xml');
+
+ expect(report.posts.items[0].markdownPreview).toBe('');
+ });
+
+ it('should preserve line breaks in text content', async () => {
+ setupDbReturns([], [], []);
+
+ const wxrData = createWxrData({
+ posts: [createWxrPost({
+ content: '
Line one\nLine two\nLine three
', + })], + }); + + const report = await engine.analyzeWxr(wxrData, '/test.xml'); + + // Line breaks within text should be preserved + expect(report.posts.items[0].markdownPreview).toContain('Line one'); + }); + }); }); /** diff --git a/tests/engine/PostMediaEngine.test.ts b/tests/engine/PostMediaEngine.test.ts index 891258b..5cbdd8d 100644 --- a/tests/engine/PostMediaEngine.test.ts +++ b/tests/engine/PostMediaEngine.test.ts @@ -538,4 +538,126 @@ describe('PostMediaEngine', () => { expect(result).toBe(false); }); }); + + describe('importMediaForPost', () => { + it('should import media and link it to the post', async () => { + const postId = 'post-1'; + const sourcePath = '/path/to/image.jpg'; + const importedMediaId = 'imported-media-123'; + + mockImportMedia.mockResolvedValue({ id: importedMediaId }); + mockGetMedia.mockResolvedValue(createMockMedia({ id: importedMediaId, linkedPostIds: [] })); + + const result = await engine.importMediaForPost(postId, sourcePath); + + expect(mockImportMedia).toHaveBeenCalledWith(sourcePath); + expect(result.postId).toBe(postId); + expect(result.mediaId).toBe(importedMediaId); + }); + }); + + describe('getLinkedMediaDataForPost', () => { + it('should return linked media with full media data', async () => { + const postId = 'post-1'; + const media1 = createMockMedia({ id: 'media-1', title: 'Image 1' }); + const media2 = createMockMedia({ id: 'media-2', title: 'Image 2' }); + + selectMockData = [ + { id: 'link-1', projectId: 'test-project', postId, mediaId: 'media-1', sortOrder: 0, createdAt: new Date() }, + { id: 'link-2', projectId: 'test-project', postId, mediaId: 'media-2', sortOrder: 1, createdAt: new Date() }, + ]; + + mockGetMedia.mockImplementation((id: string) => { + if (id === 'media-1') return Promise.resolve(media1); + if (id === 'media-2') return Promise.resolve(media2); + return Promise.resolve(null); + }); + + const result = await engine.getLinkedMediaDataForPost(postId); + + expect(result).toHaveLength(2); + expect(result[0].media.title).toBe('Image 1'); + expect(result[1].media.title).toBe('Image 2'); + }); + + it('should skip links where media is not found', async () => { + const postId = 'post-1'; + const media1 = createMockMedia({ id: 'media-1', title: 'Image 1' }); + + selectMockData = [ + { id: 'link-1', projectId: 'test-project', postId, mediaId: 'media-1', sortOrder: 0, createdAt: new Date() }, + { id: 'link-2', projectId: 'test-project', postId, mediaId: 'media-deleted', sortOrder: 1, createdAt: new Date() }, + ]; + + mockGetMedia.mockImplementation((id: string) => { + if (id === 'media-1') return Promise.resolve(media1); + return Promise.resolve(null); // media-deleted not found + }); + + const result = await engine.getLinkedMediaDataForPost(postId); + + expect(result).toHaveLength(1); + expect(result[0].media.title).toBe('Image 1'); + }); + + it('should return empty array when no links exist', async () => { + selectMockData = []; + + const result = await engine.getLinkedMediaDataForPost('post-no-links'); + + expect(result).toEqual([]); + }); + }); + + describe('edge cases for linkMediaToPost', () => { + it('should not add duplicate postId to linkedPostIds', async () => { + const postId = 'post-1'; + const mediaId = 'media-1'; + + // Media already has this post linked + mockGetMedia.mockResolvedValue(createMockMedia({ + id: mediaId, + linkedPostIds: [postId] // Already linked + })); + + await engine.linkMediaToPost(postId, mediaId); + + // updateMedia should not be called since post is already in linkedPostIds + expect(mockUpdateMedia).not.toHaveBeenCalled(); + }); + + it('should calculate correct sortOrder when existing links present', async () => { + const postId = 'post-1'; + const mediaId = 'media-new'; + + // Existing links with specific sort orders + selectMockData = [ + { id: 'link-1', projectId: 'test-project', postId, mediaId: 'media-1', sortOrder: 5, createdAt: new Date() }, + { id: 'link-2', projectId: 'test-project', postId, mediaId: 'media-2', sortOrder: 10, createdAt: new Date() }, + ]; + + mockGetMedia.mockResolvedValue(createMockMedia({ id: mediaId, linkedPostIds: [] })); + + const result = await engine.linkMediaToPost(postId, mediaId); + + // sortOrder should be max + 1 = 11 + expect(result.sortOrder).toBe(11); + }); + + it('should handle null media when linking', async () => { + const postId = 'post-1'; + const mediaId = 'media-nonexistent'; + + // Media not found + mockGetMedia.mockResolvedValue(null); + + const result = await engine.linkMediaToPost(postId, mediaId); + + // Should still create the link + expect(result.postId).toBe(postId); + expect(result.mediaId).toBe(mediaId); + // But updateMedia shouldn't be called + expect(mockUpdateMedia).not.toHaveBeenCalled(); + }); + }); });