Add comprehensive tests for Python macro integration
Co-authored-by: rfc1437 <774975+rfc1437@users.noreply.github.com>
This commit is contained in:
252
tests/engine/PageRenderer.pythonMacros.test.ts
Normal file
252
tests/engine/PageRenderer.pythonMacros.test.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
renderMacro,
|
||||
replaceAllMacrosAsync,
|
||||
isBuiltInMacro,
|
||||
normalizeMacroName,
|
||||
type PythonMacroRendererContract,
|
||||
type PythonMacroScript,
|
||||
} from '../../src/main/engine/PageRenderer';
|
||||
|
||||
describe('isBuiltInMacro', () => {
|
||||
it('returns true for all known JS built-in macro names', () => {
|
||||
expect(isBuiltInMacro('youtube')).toBe(true);
|
||||
expect(isBuiltInMacro('vimeo')).toBe(true);
|
||||
expect(isBuiltInMacro('gallery')).toBe(true);
|
||||
expect(isBuiltInMacro('photo_archive')).toBe(true);
|
||||
expect(isBuiltInMacro('tag_cloud')).toBe(true);
|
||||
expect(isBuiltInMacro('photo_album')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for unknown macro names', () => {
|
||||
expect(isBuiltInMacro('my_custom_macro')).toBe(false);
|
||||
expect(isBuiltInMacro('python_report')).toBe(false);
|
||||
expect(isBuiltInMacro('data_table')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceAllMacrosAsync', () => {
|
||||
it('replaces built-in JS macros without Python renderer', async () => {
|
||||
const result = await replaceAllMacrosAsync(
|
||||
'Before [[youtube id="abc123"]] After',
|
||||
'post-1',
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
'en',
|
||||
null,
|
||||
);
|
||||
|
||||
expect(result).toContain('class="macro-youtube"');
|
||||
expect(result).toContain('abc123');
|
||||
expect(result).not.toContain('[[youtube');
|
||||
});
|
||||
|
||||
it('replaces Python macros when renderer is provided', async () => {
|
||||
const mockRenderer: PythonMacroRendererContract = {
|
||||
getEnabledMacroScripts: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'script-1',
|
||||
slug: 'my_widget',
|
||||
entrypoint: 'render',
|
||||
content: 'def render(ctx): return {"html": "<div>Widget</div>"}',
|
||||
version: 1,
|
||||
},
|
||||
] satisfies PythonMacroScript[]),
|
||||
renderMacro: vi.fn().mockResolvedValue({
|
||||
html: '<div class="python-widget">Hello from Python</div>',
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await replaceAllMacrosAsync(
|
||||
'Before [[my_widget title="Hello"]] After',
|
||||
'post-1',
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
'en',
|
||||
mockRenderer,
|
||||
);
|
||||
|
||||
expect(result).toBe('Before <div class="python-widget">Hello from Python</div> After');
|
||||
expect(mockRenderer.renderMacro).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scriptContent: 'def render(ctx): return {"html": "<div>Widget</div>"}',
|
||||
entrypoint: 'render',
|
||||
cacheKey: 'script-1:1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves JS macros alongside Python macros', async () => {
|
||||
const mockRenderer: PythonMacroRendererContract = {
|
||||
getEnabledMacroScripts: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'script-2',
|
||||
slug: 'custom_box',
|
||||
entrypoint: 'render',
|
||||
content: 'def render(ctx): return {"html": "<aside>box</aside>"}',
|
||||
version: 3,
|
||||
},
|
||||
] satisfies PythonMacroScript[]),
|
||||
renderMacro: vi.fn().mockResolvedValue({
|
||||
html: '<aside class="custom-box">Custom Content</aside>',
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await replaceAllMacrosAsync(
|
||||
'[[youtube id="xyz789"]] then [[custom_box]]',
|
||||
'post-1',
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
'en',
|
||||
mockRenderer,
|
||||
);
|
||||
|
||||
expect(result).toContain('class="macro-youtube"');
|
||||
expect(result).toContain('xyz789');
|
||||
expect(result).toContain('<aside class="custom-box">Custom Content</aside>');
|
||||
});
|
||||
|
||||
it('returns empty string for unknown macros without Python renderer', async () => {
|
||||
const result = await replaceAllMacrosAsync(
|
||||
'Before [[unknown_macro]] After',
|
||||
'post-1',
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
'en',
|
||||
null,
|
||||
);
|
||||
|
||||
expect(result).toBe('Before After');
|
||||
});
|
||||
|
||||
it('returns empty string for unmatched Python macros', async () => {
|
||||
const mockRenderer: PythonMacroRendererContract = {
|
||||
getEnabledMacroScripts: vi.fn().mockResolvedValue([]),
|
||||
renderMacro: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await replaceAllMacrosAsync(
|
||||
'Before [[nonexistent_macro]] After',
|
||||
'post-1',
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
'en',
|
||||
mockRenderer,
|
||||
);
|
||||
|
||||
expect(result).toBe('Before After');
|
||||
expect(mockRenderer.renderMacro).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles Python macro rendering errors gracefully', async () => {
|
||||
const mockRenderer: PythonMacroRendererContract = {
|
||||
getEnabledMacroScripts: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'script-err',
|
||||
slug: 'broken_macro',
|
||||
entrypoint: 'render',
|
||||
content: 'def render(ctx): raise Exception("oops")',
|
||||
version: 1,
|
||||
},
|
||||
] satisfies PythonMacroScript[]),
|
||||
renderMacro: vi.fn().mockRejectedValue(new Error('Python execution failed')),
|
||||
};
|
||||
|
||||
const result = await replaceAllMacrosAsync(
|
||||
'Before [[broken_macro]] After',
|
||||
'post-1',
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
'en',
|
||||
mockRenderer,
|
||||
);
|
||||
|
||||
expect(result).toBe('Before After');
|
||||
});
|
||||
|
||||
it('handles script resolution errors gracefully', async () => {
|
||||
const mockRenderer: PythonMacroRendererContract = {
|
||||
getEnabledMacroScripts: vi.fn().mockRejectedValue(new Error('DB error')),
|
||||
renderMacro: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await replaceAllMacrosAsync(
|
||||
'Before [[my_macro]] After',
|
||||
'post-1',
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
'en',
|
||||
mockRenderer,
|
||||
);
|
||||
|
||||
expect(result).toBe('Before After');
|
||||
});
|
||||
|
||||
it('does not look up Python scripts when all macros are built-in', async () => {
|
||||
const mockRenderer: PythonMacroRendererContract = {
|
||||
getEnabledMacroScripts: vi.fn().mockResolvedValue([]),
|
||||
renderMacro: vi.fn(),
|
||||
};
|
||||
|
||||
await replaceAllMacrosAsync(
|
||||
'[[youtube id="a"]] [[vimeo id="1"]]',
|
||||
'post-1',
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
'en',
|
||||
mockRenderer,
|
||||
);
|
||||
|
||||
expect(mockRenderer.getEnabledMacroScripts).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes correct context to Python macro renderer', async () => {
|
||||
const mockRenderer: PythonMacroRendererContract = {
|
||||
getEnabledMacroScripts: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'ctx-script',
|
||||
slug: 'ctx_test',
|
||||
entrypoint: 'run',
|
||||
content: 'def run(ctx): return {"html": "ok"}',
|
||||
version: 2,
|
||||
},
|
||||
] satisfies PythonMacroScript[]),
|
||||
renderMacro: vi.fn().mockResolvedValue({ html: 'ok' }),
|
||||
};
|
||||
|
||||
await replaceAllMacrosAsync(
|
||||
'[[ctx_test name="Alice" count="5"]]',
|
||||
'post-42',
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
'de',
|
||||
mockRenderer,
|
||||
);
|
||||
|
||||
const call = (mockRenderer.renderMacro as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
const parsedContext = JSON.parse(call.contextJson);
|
||||
|
||||
expect(parsedContext.env.isPreview).toBe(false);
|
||||
expect(parsedContext.env.mainLanguage).toBe('de');
|
||||
expect(parsedContext.env.hook).toBe('ctx_test');
|
||||
expect(parsedContext.env.source).toEqual({ kind: 'macro', id: 'ctx-script' });
|
||||
expect(parsedContext.params).toEqual({ name: 'Alice', count: '5' });
|
||||
expect(call.entrypoint).toBe('run');
|
||||
expect(call.cacheKey).toBe('ctx-script:2');
|
||||
});
|
||||
|
||||
it('returns unchanged text when there are no macros', async () => {
|
||||
const content = 'Just plain text with no macros';
|
||||
const result = await replaceAllMacrosAsync(content, '', [], null, [], 'en', null);
|
||||
expect(result).toBe(content);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user