Backend: - Add fetcher_test.go (HTML stripping, URL fetching utilities) - Add model_registry_test.go (parsing, size ranges, model matching) - Add database_test.go (CRUD operations, migrations) - Add tests for geolocation, search, tools, version handlers Frontend unit tests (469 total): - OllamaClient: 22 tests for API methods with mocked fetch - Memory/RAG: tokenizer, chunker, summarizer, embeddings, vector-store - Services: prompt-resolution, conversation-summary - Components: Skeleton, BranchNavigator, ConfirmDialog, ThinkingBlock - Utils: export, import, file-processor, keyboard - Tools: builtin math parser (44 tests) E2E tests (28 total): - Set up Playwright with Chromium - App loading, sidebar navigation, settings page - Chat interface, responsive design, accessibility - Import dialog, project modal interactions Config changes: - Add browser conditions to vitest.config.ts for Svelte 5 components - Add playwright.config.ts for E2E testing - Add test:e2e scripts to package.json - Update .gitignore to exclude test artifacts Closes #8
212 lines
6.5 KiB
TypeScript
212 lines
6.5 KiB
TypeScript
/**
|
|
* Export utility tests
|
|
*
|
|
* Tests export formatting, filename generation, and share encoding
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
generateFilename,
|
|
generatePreview,
|
|
encodeShareableData,
|
|
decodeShareableData,
|
|
type ShareableData
|
|
} from './export';
|
|
|
|
describe('generateFilename', () => {
|
|
it('creates filename with title and timestamp', () => {
|
|
const filename = generateFilename('My Chat', 'md');
|
|
expect(filename).toMatch(/^My_Chat_\d{4}-\d{2}-\d{2}\.md$/);
|
|
});
|
|
|
|
it('replaces spaces with underscores', () => {
|
|
const filename = generateFilename('Hello World Test', 'json');
|
|
expect(filename).toContain('Hello_World_Test');
|
|
});
|
|
|
|
it('removes Windows-unsafe characters', () => {
|
|
const filename = generateFilename('File<with>special:chars/and|more?*"quotes', 'md');
|
|
expect(filename).not.toMatch(/[<>:"/\\|?*]/);
|
|
});
|
|
|
|
it('collapses multiple underscores', () => {
|
|
const filename = generateFilename('Multiple spaces here', 'md');
|
|
expect(filename).not.toContain('__');
|
|
});
|
|
|
|
it('trims leading and trailing underscores from title', () => {
|
|
const filename = generateFilename(' spaced ', 'md');
|
|
// Title part should not have leading underscore, but underscore before date is expected
|
|
expect(filename).toMatch(/^spaced_\d{4}/);
|
|
});
|
|
|
|
it('limits title length to 50 characters', () => {
|
|
const longTitle = 'a'.repeat(100);
|
|
const filename = generateFilename(longTitle, 'md');
|
|
// Should have: max 50 chars + underscore + date (10 chars) + .md (3 chars)
|
|
expect(filename.length).toBeLessThanOrEqual(50 + 1 + 10 + 3);
|
|
});
|
|
|
|
it('handles empty title', () => {
|
|
const filename = generateFilename('', 'md');
|
|
// Empty title produces underscore prefix before timestamp
|
|
expect(filename).toMatch(/_\d{4}-\d{2}-\d{2}\.md$/);
|
|
expect(filename).toContain('.md');
|
|
});
|
|
|
|
it('supports different extensions', () => {
|
|
expect(generateFilename('test', 'json')).toMatch(/\.json$/);
|
|
expect(generateFilename('test', 'txt')).toMatch(/\.txt$/);
|
|
});
|
|
});
|
|
|
|
describe('generatePreview', () => {
|
|
it('returns full content if under maxLines', () => {
|
|
const content = 'line1\nline2\nline3';
|
|
expect(generatePreview(content, 10)).toBe(content);
|
|
});
|
|
|
|
it('returns full content if exactly at maxLines', () => {
|
|
const content = 'line1\nline2\nline3';
|
|
expect(generatePreview(content, 3)).toBe(content);
|
|
});
|
|
|
|
it('truncates content over maxLines', () => {
|
|
const content = 'line1\nline2\nline3\nline4\nline5';
|
|
const preview = generatePreview(content, 3);
|
|
expect(preview).toBe('line1\nline2\nline3\n...');
|
|
});
|
|
|
|
it('uses default maxLines of 10', () => {
|
|
const lines = Array.from({ length: 15 }, (_, i) => `line${i + 1}`);
|
|
const content = lines.join('\n');
|
|
const preview = generatePreview(content);
|
|
expect(preview.split('\n').length).toBe(11); // 10 lines + "..."
|
|
expect(preview).toContain('...');
|
|
});
|
|
|
|
it('handles single line content', () => {
|
|
const content = 'single line';
|
|
expect(generatePreview(content, 5)).toBe(content);
|
|
});
|
|
|
|
it('handles empty content', () => {
|
|
expect(generatePreview('', 5)).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('encodeShareableData / decodeShareableData', () => {
|
|
const testData: ShareableData = {
|
|
version: 1,
|
|
title: 'Test Chat',
|
|
model: 'llama3:8b',
|
|
messages: [
|
|
{ role: 'user', content: 'Hello', timestamp: '2024-01-01T00:00:00Z' },
|
|
{ role: 'assistant', content: 'Hi there!', timestamp: '2024-01-01T00:00:01Z' }
|
|
]
|
|
};
|
|
|
|
it('encodes data to base64 string', () => {
|
|
const encoded = encodeShareableData(testData);
|
|
expect(typeof encoded).toBe('string');
|
|
expect(encoded.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('decodes back to original data', () => {
|
|
const encoded = encodeShareableData(testData);
|
|
const decoded = decodeShareableData(encoded);
|
|
expect(decoded).toEqual(testData);
|
|
});
|
|
|
|
it('handles UTF-8 characters', () => {
|
|
const dataWithUnicode: ShareableData = {
|
|
version: 1,
|
|
title: '你好世界 🌍',
|
|
model: 'test',
|
|
messages: [
|
|
{ role: 'user', content: 'Привет мир', timestamp: '2024-01-01T00:00:00Z' }
|
|
]
|
|
};
|
|
|
|
const encoded = encodeShareableData(dataWithUnicode);
|
|
const decoded = decodeShareableData(encoded);
|
|
expect(decoded).toEqual(dataWithUnicode);
|
|
});
|
|
|
|
it('handles special characters in content', () => {
|
|
const dataWithSpecialChars: ShareableData = {
|
|
version: 1,
|
|
title: 'Test',
|
|
model: 'test',
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: 'Code: `const x = 1;` and "quotes" and \'apostrophes\'',
|
|
timestamp: '2024-01-01T00:00:00Z'
|
|
}
|
|
]
|
|
};
|
|
|
|
const encoded = encodeShareableData(dataWithSpecialChars);
|
|
const decoded = decodeShareableData(encoded);
|
|
expect(decoded).toEqual(dataWithSpecialChars);
|
|
});
|
|
|
|
it('handles empty messages array', () => {
|
|
const dataEmpty: ShareableData = {
|
|
version: 1,
|
|
title: 'Empty',
|
|
model: 'test',
|
|
messages: []
|
|
};
|
|
|
|
const encoded = encodeShareableData(dataEmpty);
|
|
const decoded = decodeShareableData(encoded);
|
|
expect(decoded).toEqual(dataEmpty);
|
|
});
|
|
});
|
|
|
|
describe('decodeShareableData validation', () => {
|
|
it('returns null for invalid base64', () => {
|
|
expect(decodeShareableData('not-valid-base64!@#$')).toBeNull();
|
|
});
|
|
|
|
it('returns null for invalid JSON', () => {
|
|
const invalidJson = btoa(encodeURIComponent('not json'));
|
|
expect(decodeShareableData(invalidJson)).toBeNull();
|
|
});
|
|
|
|
it('returns null for missing version', () => {
|
|
const noVersion = { title: 'test', model: 'test', messages: [] };
|
|
const encoded = btoa(encodeURIComponent(JSON.stringify(noVersion)));
|
|
expect(decodeShareableData(encoded)).toBeNull();
|
|
});
|
|
|
|
it('returns null for non-numeric version', () => {
|
|
const badVersion = { version: 'one', title: 'test', model: 'test', messages: [] };
|
|
const encoded = btoa(encodeURIComponent(JSON.stringify(badVersion)));
|
|
expect(decodeShareableData(encoded)).toBeNull();
|
|
});
|
|
|
|
it('returns null for missing messages array', () => {
|
|
const noMessages = { version: 1, title: 'test', model: 'test' };
|
|
const encoded = btoa(encodeURIComponent(JSON.stringify(noMessages)));
|
|
expect(decodeShareableData(encoded)).toBeNull();
|
|
});
|
|
|
|
it('returns null for non-array messages', () => {
|
|
const badMessages = { version: 1, title: 'test', model: 'test', messages: 'not an array' };
|
|
const encoded = btoa(encodeURIComponent(JSON.stringify(badMessages)));
|
|
expect(decodeShareableData(encoded)).toBeNull();
|
|
});
|
|
|
|
it('returns valid data for minimal valid structure', () => {
|
|
const minimal = { version: 1, messages: [] };
|
|
const encoded = btoa(encodeURIComponent(JSON.stringify(minimal)));
|
|
const decoded = decodeShareableData(encoded);
|
|
expect(decoded).not.toBeNull();
|
|
expect(decoded?.version).toBe(1);
|
|
expect(decoded?.messages).toEqual([]);
|
|
});
|
|
});
|