From 5ef81d9e5ef85a0c3cb97407f24220e9f32a8ab5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:48:13 +0000 Subject: [PATCH] Add tests for PostMediaEngine and ImportAnalysisEngine branch coverage Co-authored-by: rfc1437 <774975+rfc1437@users.noreply.github.com> --- tests/engine/ImportAnalysisEngine.test.ts | 91 ++++++++++++++++ tests/engine/PostMediaEngine.test.ts | 122 ++++++++++++++++++++++ 2 files changed, 213 insertions(+) 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: 'My Image', + })], + }); + + 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('![My Image](http://example.com/full-image.jpg)'); + }); + + it('should convert linked images with non-image href (use img src)', async () => { + setupDbReturns([], [], []); + + const wxrData = createWxrData({ + posts: [createWxrPost({ + content: 'Article Image', + })], + }); + + 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('![Article Image](http://example.com/image.jpg)'); + }); + + 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('![My Title](http://example.com/full.jpg "My Title")'); + }); + + 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(); + }); + }); });