| | import { describe, expect, test, vi, beforeEach, afterEach, type MockedFunction } from 'vitest' |
| | import { |
| | createTranslationFallbackComment, |
| | EmptyTitleError, |
| | renderContentWithFallback, |
| | executeWithFallback, |
| | } from '../lib/render-with-fallback' |
| | import { TitleFromAutotitleError } from '@/content-render/unified/rewrite-local-links' |
| | import Page from '@/frame/lib/page' |
| |
|
| | |
| | type ErrorWithToken = Error & { token: { file: string; getPosition: () => number[] } } |
| | type ErrorWithTokenNoFile = Error & { token: { getPosition: () => number[] } } |
| | type ErrorWithTokenNoPosition = Error & { token: { file: string } } |
| | type ErrorWithTokenAndOriginal = Error & { |
| | token: { file: string; getPosition: () => number[] } |
| | originalError: Error |
| | } |
| |
|
| | describe('Translation Error Comments', () => { |
| | |
| | let mockRenderContent: MockedFunction< |
| | (template: string, context: Record<string, unknown>) => string |
| | > |
| |
|
| | beforeEach(() => { |
| | mockRenderContent = vi.fn() |
| | vi.stubGlobal('renderContent', mockRenderContent) |
| | }) |
| |
|
| | afterEach(() => { |
| | vi.unstubAllGlobals() |
| | }) |
| |
|
| | describe('createTranslationFallbackComment', () => { |
| | describe('Liquid ParseError', () => { |
| | test('includes all fields when token information is available', () => { |
| | const error = new Error("Unknown tag 'badtag', line:1, col:3") |
| | error.name = 'ParseError' |
| | ;(error as unknown as ErrorWithToken).token = { |
| | file: '/content/test/article.md', |
| | getPosition: () => [1, 3], |
| | } |
| |
|
| | const result = createTranslationFallbackComment(error, 'rawTitle') |
| |
|
| | expect(result).toContain('<!-- TRANSLATION_FALLBACK') |
| | expect(result).toContain('prop=rawTitle') |
| | expect(result).toContain('type=ParseError') |
| | expect(result).toContain('file=/content/test/article.md') |
| | expect(result).toContain('line=1') |
| | expect(result).toContain('col=3') |
| | expect(result).toContain("msg=\"Unknown tag 'badtag'") |
| | expect(result.endsWith('-->')).toBe(true) |
| | }) |
| | }) |
| |
|
| | describe('Liquid RenderError', () => { |
| | test('includes original error message when available', () => { |
| | const error = new Error("Unknown variable 'variables.nonexistent.value'") |
| | error.name = 'RenderError' |
| | ;(error as unknown as ErrorWithToken).token = { |
| | file: '/content/test/intro.md', |
| | getPosition: () => [3, 15], |
| | } |
| | ;(error as unknown as ErrorWithTokenAndOriginal).originalError = new Error( |
| | 'Variable not found: variables.nonexistent.value', |
| | ) |
| |
|
| | const result = createTranslationFallbackComment(error, 'rawIntro') |
| |
|
| | expect(result).toContain('prop=rawIntro') |
| | expect(result).toContain('type=RenderError') |
| | expect(result).toContain('file=/content/test/intro.md') |
| | expect(result).toContain('line=3') |
| | expect(result).toContain('col=15') |
| | expect(result).toContain('msg="Variable not found: variables.nonexistent.value"') |
| | }) |
| |
|
| | test('falls back to main error message when no originalError', () => { |
| | const error = new Error('Main error message') |
| | error.name = 'RenderError' |
| | ;(error as unknown as ErrorWithToken).token = { |
| | file: '/content/test.md', |
| | getPosition: () => [1, 1], |
| | } |
| |
|
| | const result = createTranslationFallbackComment(error, 'rawTitle') |
| |
|
| | expect(result).toContain('msg="Main error message"') |
| | }) |
| | }) |
| |
|
| | describe('Liquid TokenizationError', () => { |
| | test('includes tokenization error details', () => { |
| | const error = new Error('Unexpected token, line:1, col:10') |
| | error.name = 'TokenizationError' |
| | ;(error as unknown as ErrorWithToken).token = { |
| | file: '/content/test/page.md', |
| | getPosition: () => [1, 10], |
| | } |
| |
|
| | const result = createTranslationFallbackComment(error, 'markdown') |
| |
|
| | expect(result).toContain('prop=markdown') |
| | expect(result).toContain('type=TokenizationError') |
| | expect(result).toContain('file=/content/test/page.md') |
| | expect(result).toContain('line=1') |
| | expect(result).toContain('col=10') |
| | expect(result).toContain('msg="Unexpected token, line:1, col:10"') |
| | }) |
| | }) |
| |
|
| | describe('TitleFromAutotitleError', () => { |
| | test('includes AUTOTITLE error message', () => { |
| | const error = new TitleFromAutotitleError( |
| | 'Could not find target page for [AUTOTITLE] link to invalid-link', |
| | ) |
| | error.name = 'TitleFromAutotitleError' |
| |
|
| | const result = createTranslationFallbackComment(error, 'rawTitle') |
| |
|
| | expect(result).toContain('prop=rawTitle') |
| | expect(result).toContain('type=TitleFromAutotitleError') |
| | expect(result).toContain( |
| | 'msg="Could not find target page for [AUTOTITLE] link to invalid-link"', |
| | ) |
| | |
| | expect(result).not.toContain('file=') |
| | expect(result).not.toContain('line=') |
| | expect(result).not.toContain('col=') |
| | }) |
| | }) |
| |
|
| | describe('EmptyTitleError', () => { |
| | test('includes empty content message', () => { |
| | const error = new EmptyTitleError("output for property 'rawTitle' became empty") |
| | error.name = 'EmptyTitleError' |
| |
|
| | const result = createTranslationFallbackComment(error, 'rawTitle') |
| |
|
| | expect(result).toContain('prop=rawTitle') |
| | expect(result).toContain('type=EmptyTitleError') |
| | expect(result).toContain('msg="Content became empty after rendering"') |
| | }) |
| | }) |
| |
|
| | describe('Error handling edge cases', () => { |
| | test('handles error with no token information gracefully', () => { |
| | const error = new Error('Generic liquid error without token info') |
| | error.name = 'RenderError' |
| | |
| |
|
| | const result = createTranslationFallbackComment(error, 'rawIntro') |
| |
|
| | expect(result).toContain('prop=rawIntro') |
| | expect(result).toContain('type=RenderError') |
| | expect(result).toContain('msg="Generic liquid error without token info"') |
| | |
| | expect(result).not.toContain('file=') |
| | expect(result).not.toContain('line=') |
| | expect(result).not.toContain('col=') |
| | }) |
| |
|
| | test('handles error with token but no file', () => { |
| | const error = new Error('Error message') |
| | error.name = 'ParseError' |
| | ;(error as unknown as ErrorWithTokenNoFile).token = { |
| | |
| | getPosition: () => [5, 10], |
| | } |
| |
|
| | const result = createTranslationFallbackComment(error, 'markdown') |
| |
|
| | expect(result).toContain('line=5') |
| | expect(result).toContain('col=10') |
| | expect(result).not.toContain('file=') |
| | }) |
| |
|
| | test('handles error with token but no getPosition method', () => { |
| | const error = new Error('Error message') |
| | error.name = 'ParseError' |
| | ;(error as unknown as ErrorWithTokenNoPosition).token = { |
| | file: '/content/test.md', |
| | |
| | } |
| |
|
| | const result = createTranslationFallbackComment(error, 'title') |
| |
|
| | expect(result).toContain('file=/content/test.md') |
| | expect(result).not.toContain('line=') |
| | expect(result).not.toContain('col=') |
| | }) |
| |
|
| | test('truncates very long error messages', () => { |
| | const longMessage = 'A'.repeat(300) |
| | const error = new Error(longMessage) |
| | error.name = 'ParseError' |
| |
|
| | const result = createTranslationFallbackComment(error, 'rawTitle') |
| |
|
| | expect(result).toContain('msg="') |
| | expect(result).toContain('...') |
| |
|
| | |
| | const msgMatch = result.match(/msg="([^"]*)"/) |
| | expect(msgMatch).toBeTruthy() |
| | if (msgMatch?.[1]) { |
| | expect(msgMatch[1].length).toBeLessThanOrEqual(203) |
| | } |
| | }) |
| |
|
| | test('properly escapes quotes in error messages', () => { |
| | const error = new Error('Error with "double quotes" and more') |
| | error.name = 'RenderError' |
| |
|
| | const result = createTranslationFallbackComment(error, 'rawTitle') |
| |
|
| | expect(result).toContain('msg="Error with \'double quotes\' and more"') |
| | expect(result).not.toContain('msg="Error with "double quotes"') |
| | }) |
| |
|
| | test('handles error with unknown type', () => { |
| | const error = new Error('Some error') |
| | |
| |
|
| | const result = createTranslationFallbackComment(error, 'content') |
| |
|
| | expect(result).toContain('type=Error') |
| | expect(result).toContain('prop=content') |
| | |
| | expect(result).not.toContain('msg=') |
| | }) |
| |
|
| | test('handles error with no message', () => { |
| | const error = new Error() |
| | error.name = 'ParseError' |
| | |
| |
|
| | const result = createTranslationFallbackComment(error, 'title') |
| |
|
| | expect(result).toContain('type=ParseError') |
| | expect(result).toContain('prop=title') |
| | |
| | }) |
| |
|
| | test('cleans up multiline messages', () => { |
| | const error = new Error('Line 1\nLine 2\n Line 3 \n\nLine 5') |
| | error.name = 'RenderError' |
| |
|
| | const result = createTranslationFallbackComment(error, 'content') |
| |
|
| | expect(result).toContain('msg="Line 1 Line 2 Line 3 Line 5"') |
| | expect(result).not.toContain('\n') |
| | }) |
| | }) |
| |
|
| | describe('Comment format validation', () => { |
| | test('comment format is valid HTML', () => { |
| | const error = new Error('Test error') |
| | error.name = 'ParseError' |
| | ;(error as unknown as ErrorWithToken).token = { |
| | file: '/content/test.md', |
| | getPosition: () => [1, 1], |
| | } |
| |
|
| | const result = createTranslationFallbackComment(error, 'rawTitle') |
| |
|
| | |
| | expect(result.startsWith('<!-- TRANSLATION_FALLBACK')).toBe(true) |
| | expect(result.endsWith('-->')).toBe(true) |
| |
|
| | |
| | expect(result).not.toContain('\n') |
| | }) |
| |
|
| | test('contains all required fields when available', () => { |
| | const error = new Error('Detailed error message') |
| | error.name = 'RenderError' |
| | ;(error as unknown as ErrorWithToken).token = { |
| | file: '/content/detailed-test.md', |
| | getPosition: () => [42, 15], |
| | } |
| |
|
| | const result = createTranslationFallbackComment(error, 'rawIntro') |
| |
|
| | expect(result).toContain('TRANSLATION_FALLBACK') |
| | expect(result).toContain('prop=rawIntro') |
| | expect(result).toContain('type=RenderError') |
| | expect(result).toContain('file=/content/detailed-test.md') |
| | expect(result).toContain('line=42') |
| | expect(result).toContain('col=15') |
| | expect(result).toContain('msg="Detailed error message"') |
| | }) |
| |
|
| | test('maintains consistent field order', () => { |
| | const error = new Error('Test message') |
| | error.name = 'ParseError' |
| | ;(error as unknown as ErrorWithToken).token = { |
| | file: '/content/test.md', |
| | getPosition: () => [1, 1], |
| | } |
| |
|
| | const result = createTranslationFallbackComment(error, 'title') |
| |
|
| | |
| | expect(result.startsWith('<!-- TRANSLATION_FALLBACK')).toBe(true) |
| | expect(result).toContain('prop=title') |
| | expect(result).toContain('type=ParseError') |
| | expect(result).toContain('file=/content/test.md') |
| | expect(result).toContain('line=1') |
| | expect(result).toContain('col=1') |
| | expect(result).toContain('msg="Test message"') |
| | expect(result.endsWith('-->')).toBe(true) |
| | }) |
| | }) |
| | }) |
| |
|
| | describe('Integration Tests', () => { |
| | describe('renderContentWithFallback', () => { |
| | test('adds HTML comment when translation fails and fallback succeeds', async () => { |
| | |
| | const mockPage = Object.create(Page.prototype) |
| | mockPage.rawTitle = '{% badtag %}' |
| |
|
| | const context = { |
| | currentLanguage: 'ja', |
| | getEnglishPage: () => { |
| | const enPage = Object.create(Page.prototype) |
| | enPage.rawTitle = 'English Title' |
| | return enPage |
| | }, |
| | } |
| |
|
| | |
| | mockRenderContent.mockImplementation( |
| | (template: string, innerContext: Record<string, unknown>) => { |
| | if (innerContext.currentLanguage !== 'en' && template.includes('badtag')) { |
| | const error = new Error("Unknown tag 'badtag'") |
| | error.name = 'ParseError' |
| | ;(error as unknown as ErrorWithToken).token = { |
| | file: '/content/test.md', |
| | getPosition: () => [1, 5], |
| | } |
| | throw error |
| | } |
| | return innerContext.currentLanguage === 'en' ? 'English Title' : template |
| | }, |
| | ) |
| |
|
| | const result = await renderContentWithFallback(mockPage, 'rawTitle', context) |
| |
|
| | expect(result).toContain('<!-- TRANSLATION_FALLBACK') |
| | expect(result).toContain('prop=rawTitle') |
| | expect(result).toContain('type=ParseError') |
| | expect(result).toContain('line=1') |
| | expect(result).toContain('col=1') |
| | expect(result).toContain('msg="tag \'badtag\' not found"') |
| | expect(result).toContain('English Title') |
| | }) |
| |
|
| | test('does not add comment for textOnly rendering', async () => { |
| | const mockPage = Object.create(Page.prototype) |
| | mockPage.rawTitle = '{% badtag %}' |
| |
|
| | const context = { |
| | currentLanguage: 'ja', |
| | getEnglishPage: () => { |
| | const enPage = Object.create(Page.prototype) |
| | enPage.rawTitle = 'English Title' |
| | return enPage |
| | }, |
| | } |
| |
|
| | mockRenderContent.mockImplementation( |
| | (template: string, innerContext: Record<string, unknown>) => { |
| | if (innerContext.currentLanguage !== 'en' && template.includes('badtag')) { |
| | const error = new Error("Unknown tag 'badtag'") |
| | error.name = 'ParseError' |
| | throw error |
| | } |
| | return 'English Title' |
| | }, |
| | ) |
| |
|
| | const result = await renderContentWithFallback(mockPage, 'rawTitle', context, { |
| | textOnly: true, |
| | }) |
| |
|
| | expect(result).not.toContain('<!-- TRANSLATION_FALLBACK') |
| | expect(result).toBe('English Title') |
| | }) |
| | }) |
| |
|
| | describe('executeWithFallback', () => { |
| | test('adds HTML comment for HTML content when callable fails', async () => { |
| | const context = { |
| | currentLanguage: 'es', |
| | } |
| |
|
| | const failingCallable = async () => { |
| | const error = new Error("Unknown variable 'variables.bad'") |
| | error.name = 'RenderError' |
| | ;(error as unknown as ErrorWithToken).token = { |
| | file: '/content/article.md', |
| | getPosition: () => [10, 20], |
| | } |
| | throw error |
| | } |
| |
|
| | const fallbackCallable = async () => '<div>English fallback content</div>' |
| |
|
| | const result = await executeWithFallback(context, failingCallable, fallbackCallable) |
| |
|
| | expect(result).toContain('<!-- TRANSLATION_FALLBACK') |
| | expect(result).toContain('prop=content') |
| | expect(result).toContain('type=RenderError') |
| | expect(result).toContain('file=/content/article.md') |
| | expect(result).toContain('line=10') |
| | expect(result).toContain('col=20') |
| | expect(result).toContain('<div>English fallback content</div>') |
| | }) |
| |
|
| | test('does not add comment for non-HTML content', async () => { |
| | const context = { |
| | currentLanguage: 'fr', |
| | } |
| |
|
| | const failingCallable = async () => { |
| | const error = new Error('Test error') |
| | error.name = 'RenderError' |
| | throw error |
| | } |
| |
|
| | const fallbackCallable = async () => 'Plain text fallback' |
| |
|
| | const result = await executeWithFallback(context, failingCallable, fallbackCallable) |
| |
|
| | expect(result).not.toContain('<!-- TRANSLATION_FALLBACK') |
| | expect(result).toBe('Plain text fallback') |
| | }) |
| | }) |
| | }) |
| | }) |
| |
|