Files
obsidian-mcp-server/tests/frontmatter-utils.test.ts
Bill edcc434e93 test: add decompression failure handling and test coverage
Add base64 validation and error handling for compressed Excalidraw data:
- Validate compressed data using atob() before processing
- Add console.error logging for decompression failures
- Handle invalid base64 gracefully with fallback metadata
- Add test for decompression failure scenario

This improves frontmatter-utils coverage from 95.9% to 98.36%.
Remaining uncovered lines (301-303) are Buffer.from fallback for
environments without atob, which is expected and acceptable.
2025-10-25 22:14:29 -04:00

924 lines
28 KiB
TypeScript

import { FrontmatterUtils } from '../src/utils/frontmatter-utils';
// Mock the parseYaml function from obsidian
jest.mock('obsidian', () => ({
parseYaml: jest.fn()
}));
import { parseYaml } from 'obsidian';
const mockParseYaml = parseYaml as jest.MockedFunction<typeof parseYaml>;
describe('FrontmatterUtils', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('extractFrontmatter()', () => {
describe('valid frontmatter with --- delimiters', () => {
test('extracts frontmatter with Unix line endings', () => {
const content = '---\ntitle: Test\ntags: [tag1, tag2]\n---\nContent here';
mockParseYaml.mockReturnValue({ title: 'Test', tags: ['tag1', 'tag2'] });
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(true);
expect(result.frontmatter).toBe('title: Test\ntags: [tag1, tag2]');
expect(result.parsedFrontmatter).toEqual({ title: 'Test', tags: ['tag1', 'tag2'] });
expect(result.contentWithoutFrontmatter).toBe('Content here');
expect(result.content).toBe(content);
expect(mockParseYaml).toHaveBeenCalledWith('title: Test\ntags: [tag1, tag2]');
});
test('extracts frontmatter with Windows line endings (\\r\\n)', () => {
const content = '---\r\ntitle: Test\r\n---\r\nContent';
mockParseYaml.mockReturnValue({ title: 'Test' });
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(true);
expect(result.frontmatter).toBe('title: Test\r');
expect(result.parsedFrontmatter).toEqual({ title: 'Test' });
});
test('extracts frontmatter with ... closing delimiter', () => {
const content = '---\ntitle: Test\n...\nContent here';
mockParseYaml.mockReturnValue({ title: 'Test' });
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(true);
expect(result.frontmatter).toBe('title: Test');
expect(result.parsedFrontmatter).toEqual({ title: 'Test' });
expect(result.contentWithoutFrontmatter).toBe('Content here');
});
test('extracts frontmatter with whitespace in closing delimiter line', () => {
const content = '---\ntitle: Test\n--- \nContent here';
mockParseYaml.mockReturnValue({ title: 'Test' });
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(true);
expect(result.frontmatter).toBe('title: Test');
expect(result.contentWithoutFrontmatter).toBe('Content here');
});
test('extracts empty frontmatter', () => {
const content = '---\n---\nContent here';
mockParseYaml.mockReturnValue({});
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(true);
expect(result.frontmatter).toBe('');
expect(result.parsedFrontmatter).toEqual({});
expect(result.contentWithoutFrontmatter).toBe('Content here');
});
test('handles multiline frontmatter values', () => {
const content = '---\ntitle: Test\ndescription: |\n Line 1\n Line 2\n---\nContent';
mockParseYaml.mockReturnValue({
title: 'Test',
description: 'Line 1\nLine 2'
});
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(true);
expect(result.frontmatter).toBe('title: Test\ndescription: |\n Line 1\n Line 2');
expect(result.parsedFrontmatter).toEqual({
title: 'Test',
description: 'Line 1\nLine 2'
});
});
});
describe('no frontmatter', () => {
test('handles content without frontmatter', () => {
const content = 'Just regular content';
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(false);
expect(result.frontmatter).toBe('');
expect(result.parsedFrontmatter).toBe(null);
expect(result.content).toBe(content);
expect(result.contentWithoutFrontmatter).toBe(content);
expect(mockParseYaml).not.toHaveBeenCalled();
});
test('handles content starting with --- not at beginning', () => {
const content = 'Some text\n---\ntitle: Test\n---';
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(false);
expect(result.frontmatter).toBe('');
expect(result.parsedFrontmatter).toBe(null);
});
test('handles empty string', () => {
const content = '';
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(false);
expect(result.frontmatter).toBe('');
expect(result.parsedFrontmatter).toBe(null);
expect(result.content).toBe('');
expect(result.contentWithoutFrontmatter).toBe('');
});
});
describe('missing closing delimiter', () => {
test('treats missing closing delimiter as no frontmatter', () => {
const content = '---\ntitle: Test\nmore content';
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(false);
expect(result.frontmatter).toBe('');
expect(result.parsedFrontmatter).toBe(null);
expect(result.content).toBe(content);
expect(result.contentWithoutFrontmatter).toBe(content);
expect(mockParseYaml).not.toHaveBeenCalled();
});
test('handles single line with just opening delimiter', () => {
const content = '---';
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(false);
expect(result.parsedFrontmatter).toBe(null);
});
});
describe('parse errors', () => {
test('handles parseYaml throwing error', () => {
const content = '---\ninvalid: yaml: content:\n---\nContent';
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
mockParseYaml.mockImplementation(() => {
throw new Error('Invalid YAML');
});
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(true);
expect(result.frontmatter).toBe('invalid: yaml: content:');
expect(result.parsedFrontmatter).toBe(null);
expect(result.contentWithoutFrontmatter).toBe('Content');
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to parse frontmatter:',
expect.any(Error)
);
consoleErrorSpy.mockRestore();
});
test('handles parseYaml returning null', () => {
const content = '---\ntitle: Test\n---\nContent';
mockParseYaml.mockReturnValue(null);
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(true);
expect(result.parsedFrontmatter).toEqual({});
});
test('handles parseYaml returning undefined', () => {
const content = '---\ntitle: Test\n---\nContent';
mockParseYaml.mockReturnValue(undefined);
const result = FrontmatterUtils.extractFrontmatter(content);
expect(result.hasFrontmatter).toBe(true);
expect(result.parsedFrontmatter).toEqual({});
});
});
});
describe('extractFrontmatterSummary()', () => {
test('returns null for null input', () => {
const result = FrontmatterUtils.extractFrontmatterSummary(null);
expect(result).toBe(null);
});
test('returns null for empty object', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({});
expect(result).toBe(null);
});
test('extracts title field', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({ title: 'My Title' });
expect(result).toEqual({ title: 'My Title' });
});
test('extracts tags as array', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({ tags: ['tag1', 'tag2'] });
expect(result).toEqual({ tags: ['tag1', 'tag2'] });
});
test('converts tags from string to array', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({ tags: 'single-tag' });
expect(result).toEqual({ tags: ['single-tag'] });
});
test('extracts aliases as array', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({ aliases: ['alias1', 'alias2'] });
expect(result).toEqual({ aliases: ['alias1', 'alias2'] });
});
test('converts aliases from string to array', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({ aliases: 'single-alias' });
expect(result).toEqual({ aliases: ['single-alias'] });
});
test('extracts all common fields together', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({
title: 'My Note',
tags: ['tag1', 'tag2'],
aliases: 'my-alias'
});
expect(result).toEqual({
title: 'My Note',
tags: ['tag1', 'tag2'],
aliases: ['my-alias']
});
});
test('includes other top-level fields', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({
title: 'My Note',
author: 'John Doe',
date: '2025-01-20',
custom: 'value'
});
expect(result).toEqual({
title: 'My Note',
author: 'John Doe',
date: '2025-01-20',
custom: 'value'
});
});
test('does not duplicate common fields in other fields', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({
title: 'My Note',
tags: ['tag1'],
aliases: ['alias1']
});
// Should have these fields exactly once
expect(result).toEqual({
title: 'My Note',
tags: ['tag1'],
aliases: ['alias1']
});
expect(Object.keys(result!).length).toBe(3);
});
test('ignores non-standard tag types (not string or array)', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({
tags: 123, // Not a string or array - skipped in normalization
other: 'value'
});
// Tags are not string/array, so skipped during normalization
// The loop excludes 'tags' key from other fields, so tags won't appear
expect(result).toEqual({ other: 'value' });
});
test('ignores non-standard alias types (not string or array)', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({
aliases: true, // Not a string or array - skipped in normalization
other: 'value'
});
// Aliases are not string/array, so skipped during normalization
// The loop excludes 'aliases' key from other fields, so aliases won't appear
expect(result).toEqual({ other: 'value' });
});
test('handles frontmatter with only unrecognized fields', () => {
const result = FrontmatterUtils.extractFrontmatterSummary({
custom1: 'value1',
custom2: 'value2'
});
expect(result).toEqual({
custom1: 'value1',
custom2: 'value2'
});
});
});
describe('hasFrontmatter()', () => {
test('returns true for content with Unix line endings', () => {
expect(FrontmatterUtils.hasFrontmatter('---\ntitle: Test\n---\n')).toBe(true);
});
test('returns true for content with Windows line endings', () => {
expect(FrontmatterUtils.hasFrontmatter('---\r\ntitle: Test\r\n---\r\n')).toBe(true);
});
test('returns false for content without frontmatter', () => {
expect(FrontmatterUtils.hasFrontmatter('Just content')).toBe(false);
});
test('returns false for content with --- not at start', () => {
expect(FrontmatterUtils.hasFrontmatter('Some text\n---\n')).toBe(false);
});
test('returns false for empty string', () => {
expect(FrontmatterUtils.hasFrontmatter('')).toBe(false);
});
test('returns false for content starting with -- (only two dashes)', () => {
expect(FrontmatterUtils.hasFrontmatter('--\ntitle: Test')).toBe(false);
});
});
describe('serializeFrontmatter()', () => {
test('returns empty string for empty object', () => {
expect(FrontmatterUtils.serializeFrontmatter({})).toBe('');
});
test('returns empty string for null', () => {
expect(FrontmatterUtils.serializeFrontmatter(null as any)).toBe('');
});
test('returns empty string for undefined', () => {
expect(FrontmatterUtils.serializeFrontmatter(undefined as any)).toBe('');
});
test('serializes simple string values', () => {
const result = FrontmatterUtils.serializeFrontmatter({ title: 'Test' });
expect(result).toBe('---\ntitle: Test\n---');
});
test('serializes number values', () => {
const result = FrontmatterUtils.serializeFrontmatter({ count: 42 });
expect(result).toBe('---\ncount: 42\n---');
});
test('serializes boolean values', () => {
const result = FrontmatterUtils.serializeFrontmatter({
published: true,
draft: false
});
expect(result).toBe('---\npublished: true\ndraft: false\n---');
});
test('serializes arrays with items', () => {
const result = FrontmatterUtils.serializeFrontmatter({
tags: ['tag1', 'tag2', 'tag3']
});
expect(result).toBe('---\ntags:\n - tag1\n - tag2\n - tag3\n---');
});
test('serializes empty arrays', () => {
const result = FrontmatterUtils.serializeFrontmatter({ tags: [] });
expect(result).toBe('---\ntags: []\n---');
});
test('serializes arrays with non-string items', () => {
const result = FrontmatterUtils.serializeFrontmatter({
numbers: [1, 2, 3],
mixed: ['text', 42, true]
});
expect(result).toContain('numbers:\n - 1\n - 2\n - 3');
expect(result).toContain('mixed:\n - text\n - 42\n - true');
});
test('serializes nested objects', () => {
const result = FrontmatterUtils.serializeFrontmatter({
metadata: { author: 'John', year: 2025 }
});
expect(result).toBe('---\nmetadata:\n author: John\n year: 2025\n---');
});
test('quotes strings with special characters (colon)', () => {
const result = FrontmatterUtils.serializeFrontmatter({
title: 'Note: Important'
});
expect(result).toBe('---\ntitle: "Note: Important"\n---');
});
test('quotes strings with special characters (hash)', () => {
const result = FrontmatterUtils.serializeFrontmatter({
tag: '#important'
});
expect(result).toBe('---\ntag: "#important"\n---');
});
test('quotes strings with special characters (brackets)', () => {
const result = FrontmatterUtils.serializeFrontmatter({
link: '[link]',
array: '[[link]]'
});
expect(result).toContain('link: "[link]"');
expect(result).toContain('array: "[[link]]"');
});
test('quotes strings with special characters (braces)', () => {
const result = FrontmatterUtils.serializeFrontmatter({
template: '{variable}'
});
expect(result).toBe('---\ntemplate: "{variable}"\n---');
});
test('quotes strings with special characters (pipe)', () => {
const result = FrontmatterUtils.serializeFrontmatter({
option: 'a|b'
});
expect(result).toBe('---\noption: "a|b"\n---');
});
test('quotes strings with special characters (greater than)', () => {
const result = FrontmatterUtils.serializeFrontmatter({
text: '>quote'
});
expect(result).toBe('---\ntext: ">quote"\n---');
});
test('quotes strings with leading whitespace', () => {
const result = FrontmatterUtils.serializeFrontmatter({
text: ' leading'
});
expect(result).toBe('---\ntext: " leading"\n---');
});
test('quotes strings with trailing whitespace', () => {
const result = FrontmatterUtils.serializeFrontmatter({
text: 'trailing '
});
expect(result).toBe('---\ntext: "trailing "\n---');
});
test('escapes quotes in quoted strings', () => {
const result = FrontmatterUtils.serializeFrontmatter({
title: 'Note: "Important"'
});
expect(result).toBe('---\ntitle: "Note: \\"Important\\""\n---');
});
test('handles multiple quotes in string', () => {
const result = FrontmatterUtils.serializeFrontmatter({
text: 'She said: "Hello" and "Goodbye"'
});
expect(result).toBe('---\ntext: "She said: \\"Hello\\" and \\"Goodbye\\""\n---');
});
test('skips undefined values', () => {
const result = FrontmatterUtils.serializeFrontmatter({
title: 'Test',
skipped: undefined,
kept: 'value'
});
expect(result).toBe('---\ntitle: Test\nkept: value\n---');
expect(result).not.toContain('skipped');
});
test('skips null values', () => {
const result = FrontmatterUtils.serializeFrontmatter({
title: 'Test',
skipped: null,
kept: 'value'
});
expect(result).toBe('---\ntitle: Test\nkept: value\n---');
expect(result).not.toContain('skipped');
});
test('serializes complex nested structures', () => {
const result = FrontmatterUtils.serializeFrontmatter({
title: 'Complex Note',
tags: ['tag1', 'tag2'],
metadata: {
author: 'John',
version: 1
},
published: true
});
expect(result).toContain('title: Complex Note');
expect(result).toContain('tags:\n - tag1\n - tag2');
expect(result).toContain('metadata:\n author: John\n version: 1');
expect(result).toContain('published: true');
});
test('uses JSON.stringify as fallback for unknown types', () => {
const result = FrontmatterUtils.serializeFrontmatter({
custom: Symbol('test') as any
});
// Symbol can't be JSON stringified, but the fallback should handle it
expect(result).toContain('custom:');
});
});
describe('parseExcalidrawMetadata()', () => {
describe('Excalidraw marker detection', () => {
test('detects excalidraw-plugin marker', () => {
const content = '# Drawing\nSome text with excalidraw-plugin marker';
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
});
test('detects type:excalidraw marker', () => {
const content = '{"type":"excalidraw"}';
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
});
test('returns false for non-Excalidraw content', () => {
const content = 'Just a regular note';
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(false);
expect(result.elementCount).toBeUndefined();
expect(result.hasCompressedData).toBeUndefined();
expect(result.metadata).toBeUndefined();
});
});
describe('JSON extraction from code blocks', () => {
test('extracts JSON from compressed-json code block after ## Drawing', () => {
const content = `# Text Elements
excalidraw-plugin
Text content
## Drawing
\`\`\`compressed-json
N4KAkARALgngDgUwgLgAQQQDwMYEMA2AlgCYBOuA7hADTgQBuCpAzoQPYB2KqATL
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.hasCompressedData).toBe(true);
expect(result.metadata?.compressed).toBe(true);
});
test('extracts JSON from json code block after ## Drawing', () => {
const content = `## Drawing
\`\`\`json
{"elements": [{"id": "1"}, {"id": "2"}], "appState": {}, "version": 2, "type":"excalidraw"}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(2);
expect(result.hasCompressedData).toBe(false);
expect(result.metadata?.version).toBe(2);
});
test('extracts JSON from code block with any language specifier', () => {
const content = `## Drawing
\`\`\`javascript
{"elements": [{"id": "1"}], "appState": {}, "version": 2, "type":"excalidraw"}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(1);
});
test('extracts JSON from code block with language specifier after ## Drawing (pattern 3)', () => {
const content = `excalidraw-plugin
## Drawing
Not compressed-json or json language, but has a language specifier
\`\`\`typescript
{"elements": [{"id": "1"}], "appState": {}, "version": 2}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(1);
});
test('extracts JSON from code block without language specifier', () => {
const content = `## Drawing
\`\`\`
{"elements": [], "appState": {}, "version": 2, "type":"excalidraw"}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(0);
});
test('extracts JSON from code block without language after ## Drawing (pattern 4)', () => {
const content = `excalidraw-plugin
## Drawing
No compressed-json, json, or other language specifier
\`\`\`
{"elements": [{"id": "1"}, {"id": "2"}], "appState": {}, "version": 2}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(2);
});
test('parses Excalidraw with code fence lacking language specifier (coverage for lines 253-255)', () => {
// Specific test to ensure Pattern 4 code path is exercised
// Uses only basic code fence with no language hint after ## Drawing
const content = `
excalidraw-plugin
## Drawing
\`\`\`
{"elements": [{"id": "elem1"}, {"id": "elem2"}, {"id": "elem3"}], "appState": {"gridSize": 20}, "version": 2}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(3);
expect(result.hasCompressedData).toBe(false);
expect(result.metadata?.version).toBe(2);
expect(result.metadata?.appState).toEqual({"gridSize": 20});
});
test('tries patterns in entire content if no ## Drawing section', () => {
const content = `\`\`\`json
{"elements": [{"id": "1"}], "appState": {}, "version": 2, "type":"excalidraw"}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(1);
});
test('handles missing JSON block with default values', () => {
const content = '# Text\nexcalidraw-plugin marker but no JSON';
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(0);
expect(result.hasCompressedData).toBe(false);
expect(result.metadata).toEqual({});
});
});
describe('compressed data handling', () => {
test('detects compressed data starting with N4KAk', () => {
const content = `## Drawing
\`\`\`json
N4KAkARALgngDgUwgLgAQQQDwMYEMA2AlgCYBOuA7hADTgQBuCpAzoQPYB2KqATL
\`\`\`
excalidraw-plugin`;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.hasCompressedData).toBe(true);
expect(result.elementCount).toBe(0);
expect(result.metadata?.compressed).toBe(true);
});
test('detects compressed data not starting with {', () => {
const content = `## Drawing
\`\`\`json
ABC123CompressedData
\`\`\`
excalidraw-plugin`;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.hasCompressedData).toBe(true);
});
});
describe('uncompressed JSON parsing', () => {
test('parses valid JSON with elements', () => {
const content = `excalidraw-plugin
## Drawing
\`\`\`json
{
"elements": [
{"id": "1", "type": "rectangle"},
{"id": "2", "type": "arrow"}
],
"appState": {"viewBackgroundColor": "#fff"},
"version": 2
}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(2);
expect(result.hasCompressedData).toBe(false);
expect(result.metadata?.appState).toEqual({ viewBackgroundColor: '#fff' });
expect(result.metadata?.version).toBe(2);
});
test('handles missing elements array', () => {
const content = `excalidraw-plugin
\`\`\`json
{"appState": {}, "version": 2}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(0);
});
test('detects compressed files data', () => {
const content = `excalidraw-plugin
\`\`\`json
{
"elements": [],
"appState": {},
"version": 2,
"files": {
"file1": {"data": "base64data"}
}
}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.hasCompressedData).toBe(true);
});
test('handles empty files object as not compressed', () => {
const content = `excalidraw-plugin
\`\`\`json
{"elements": [], "appState": {}, "version": 2, "files": {}}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.hasCompressedData).toBe(false);
});
test('uses default version if missing', () => {
const content = `excalidraw-plugin
\`\`\`json
{"elements": [], "appState": {}}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.metadata?.version).toBe(2);
});
test('uses empty appState if missing', () => {
const content = `excalidraw-plugin
\`\`\`json
{"elements": [], "version": 2}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.metadata?.appState).toEqual({});
});
});
describe('error handling', () => {
test('handles decompression failure gracefully', () => {
// Mock atob to throw an error to simulate decompression failure
// This covers the catch block for compressed data decompression errors
const originalAtob = global.atob;
global.atob = jest.fn(() => {
throw new Error('Invalid base64 string');
});
const content = `excalidraw-plugin
## Drawing
\`\`\`compressed-json
N4KAkARALgngDgUwgLgAQQQDwMYEMA2AlgCYBOuA7hADTgQBuCpAzoQPYB2KqATL
\`\`\``;
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(0);
expect(result.hasCompressedData).toBe(true);
expect(result.metadata).toEqual({ compressed: true });
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to process compressed Excalidraw data:',
expect.anything()
);
consoleErrorSpy.mockRestore();
global.atob = originalAtob;
});
test('handles JSON parse error gracefully', () => {
const content = `excalidraw-plugin
\`\`\`json
{invalid json content}
\`\`\``;
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(0);
expect(result.hasCompressedData).toBe(false);
expect(result.metadata).toEqual({});
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Excalidraw parsing error:',
expect.any(Error)
);
consoleErrorSpy.mockRestore();
});
test('handles error when no Excalidraw marker present', () => {
const content = `\`\`\`json
{invalid json}
\`\`\``;
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(false);
expect(result.elementCount).toBeUndefined();
expect(result.hasCompressedData).toBeUndefined();
expect(result.metadata).toBeUndefined();
consoleErrorSpy.mockRestore();
});
test('logs error but returns valid result structure', () => {
const content = 'excalidraw-plugin with error causing content';
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
// Force an error by making content throw during processing
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
// Should still return valid structure
expect(result).toHaveProperty('isExcalidraw');
consoleErrorSpy.mockRestore();
});
});
describe('edge cases', () => {
test('handles content with multiple code blocks', () => {
const content = `excalidraw-plugin
\`\`\`python
print("hello")
\`\`\`
## Drawing
\`\`\`json
{"elements": [{"id": "1"}], "appState": {}, "version": 2}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(1);
});
test('handles whitespace variations in code fence', () => {
const content = `excalidraw-plugin
## Drawing
\`\`\`json
{"elements": [], "appState": {}, "version": 2}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(0);
});
test('handles JSON with extra whitespace', () => {
const content = `excalidraw-plugin
\`\`\`json
{"elements": [], "appState": {}, "version": 2}
\`\`\``;
const result = FrontmatterUtils.parseExcalidrawMetadata(content);
expect(result.isExcalidraw).toBe(true);
expect(result.elementCount).toBe(0);
});
});
});
});