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 aliases for error objects with token information 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', () => { // Mock renderContent for integration tests let mockRenderContent: MockedFunction< (template: string, context: Record) => 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('')).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"', ) // Should not contain file/line/col since AUTOTITLE errors don't have tokens 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' // No token property 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"') // Should not contain file/line/col since no token 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 = { // No file property 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', // No getPosition method } 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) // Very long error message const error = new Error(longMessage) error.name = 'ParseError' const result = createTranslationFallbackComment(error, 'rawTitle') expect(result).toContain('msg="') expect(result).toContain('...') // Extract the message part to verify truncation const msgMatch = result.match(/msg="([^"]*)"/) expect(msgMatch).toBeTruthy() if (msgMatch?.[1]) { expect(msgMatch[1].length).toBeLessThanOrEqual(203) // 200 + '...' } }) 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') // No name property (will default to 'Error') const result = createTranslationFallbackComment(error, 'content') expect(result).toContain('type=Error') expect(result).toContain('prop=content') // Non-liquid errors without specific handling don't get messages expect(result).not.toContain('msg=') }) test('handles error with no message', () => { const error = new Error() error.name = 'ParseError' // Message will be empty string by default const result = createTranslationFallbackComment(error, 'title') expect(result).toContain('type=ParseError') expect(result).toContain('prop=title') // Should handle gracefully, might not have msg or have empty msg }) 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') // Should be a proper HTML comment expect(result.startsWith('')).toBe(true) // Should be on a single line 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') // Should follow the expected structure with all required fields expect(result.startsWith('')).toBe(true) }) }) }) describe('Integration Tests', () => { describe('renderContentWithFallback', () => { test('adds HTML comment when translation fails and fallback succeeds', async () => { // Mock a simple page object that satisfies instanceof Page check 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 }, } // Mock renderContent to simulate error for Japanese, success for English mockRenderContent.mockImplementation( (template: string, innerContext: Record) => { 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('