HubMark employs comprehensive testing across all layers to ensure reliability, cross-browser compatibility, and robust error handling.
graph TB
subgraph "Testing Pyramid"
A[Unit Tests<br/>~80% Coverage]
B[Integration Tests<br/>~15% Coverage]
C[E2E Tests<br/>~5% Coverage]
end
A --> D[Fast Feedback]
B --> E[Component Integration]
C --> F[User Journey Validation]
subgraph "Test Types"
G[Functionality Tests]
H[Error Handling Tests]
I[Browser Compatibility Tests]
J[Performance Tests]
end
| Utility | Tests | Coverage | Status |
|---|---|---|---|
storage.ts |
17/17 | 100% | ✅ Passing |
github.ts |
34/34 | 100% | ✅ Passing |
bookmarks.ts |
24/24 | 100% | ✅ Passing |
sync.ts |
TBD | TBD | 🚧 Pending |
Total: 75/75 tests passing (100%)
graph LR
subgraph "Test Framework Stack"
A[Vitest<br/>Test Runner]
B[@testing-library<br/>Component Testing]
C[jsdom<br/>Browser Environment]
end
subgraph "Mock Strategy"
D[Browser API Mocks]
E[GitHub API Mocks]
F[Storage Mocks]
end
A --> D
B --> E
C --> F
Storage API Mock:
const mockBrowser = {
storage: {
sync: {
get: vi.fn().mockResolvedValue({}),
set: vi.fn().mockResolvedValue(undefined),
clear: vi.fn().mockResolvedValue(undefined),
},
local: {
get: vi.fn().mockResolvedValue({}),
set: vi.fn().mockResolvedValue(undefined),
clear: vi.fn().mockResolvedValue(undefined),
},
},
};
global.browser = mockBrowser;Bookmarks API Mock:
const mockBookmarksAPI = {
bookmarks: {
getTree: vi.fn(),
get: vi.fn(),
getChildren: vi.fn(),
create: vi.fn(),
update: vi.fn(),
remove: vi.fn(),
},
};Octokit Mock Strategy:
const mockOctokit = {
rest: {
users: { getAuthenticated: vi.fn() },
repos: {
listForAuthenticatedUser: vi.fn(),
createForAuthenticatedUser: vi.fn(),
get: vi.fn(),
createOrUpdateFileContents: vi.fn(),
deleteFile: vi.fn(),
getContent: vi.fn(),
},
git: { createRef: vi.fn() },
},
};
vi.mock('@octokit/rest', () => ({
Octokit: vi.fn(() => mockOctokit),
}));Bookmark Test Data:
const createMockBookmark = (overrides = {}): NormalizedBookmark => ({
id: 'hm_test123',
title: 'Test Bookmark',
url: 'https://example.com',
folderPath: 'Test Folder',
tags: ['test'],
notes: 'Test notes',
dateAdded: Date.now(),
dateModified: Date.now(),
...overrides,
});
const createBrowserBookmark = (overrides = {}): BrowserBookmark => ({
id: 'browser_123',
title: 'Test Bookmark #test (Test notes)',
url: 'https://example.com',
dateAdded: Date.now(),
...overrides,
});CRUD Operations:
describe('Storage Manager CRUD', () => {
it('should create and retrieve bookmark', async () => {
const bookmark = createMockBookmark();
await storageManager.addBookmark(bookmark);
const retrieved = await storageManager.getBookmarks();
expect(retrieved).toContain(expect.objectContaining({
id: bookmark.id,
title: bookmark.title,
}));
});
});Data Transformation:
describe('Markdown Generation', () => {
it('should generate markdown with folder organization', () => {
const bookmarks = [
createMockBookmark({ folder: 'Development' }),
createMockBookmark({ folder: 'Personal' }),
];
const markdown = generateMarkdownContent(bookmarks, 'folder');
expect(markdown).toContain('## Development');
expect(markdown).toContain('## Personal');
});
});Network Failures:
describe('GitHub Client Error Handling', () => {
it('should handle authentication failures gracefully', async () => {
mockOctokit.rest.users.getAuthenticated
.mockRejectedValue(new Error('Unauthorized'));
await expect(github.authenticate())
.rejects.toThrow('GitHub authentication failed: Unauthorized');
});
it('should handle rate limiting', async () => {
const rateLimitError = new Error('Rate limit exceeded');
rateLimitError.status = 429;
mockOctokit.rest.repos.createOrUpdateFileContents
.mockRejectedValue(rateLimitError);
await expect(github.createFile('test.md', 'content', 'message'))
.rejects.toThrow('Failed to create file: Rate limit exceeded');
});
});Validation Errors:
describe('Bookmark Validation', () => {
it('should reject bookmarks without titles', () => {
expect(() => validateBookmarkData({ url: 'https://example.com' }))
.toThrow('Bookmark title is required');
});
it('should reject invalid URLs', () => {
expect(() => validateBookmarkData({
title: 'Test',
url: 'not-a-url'
})).toThrow('Invalid bookmark URL');
});
});Cross-Browser ID Mapping:
describe('ID Stability', () => {
it('should generate consistent IDs across browser sessions', () => {
const bookmark1 = { url: 'https://example.com', title: 'Test' };
const bookmark2 = { url: 'https://example.com', title: 'Test' };
const id1 = generateStableId(bookmark1.url, bookmark1.title);
const id2 = generateStableId(bookmark2.url, bookmark2.title);
expect(id1.split('_')[1]).toBe(id2.split('_')[1]); // Hash part should match
});
});Browser Detection:
describe('Browser Detection', () => {
beforeEach(() => {
// Mock different user agents
});
it('should detect Chrome correctly', () => {
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 Chrome/91.0.4472.124',
configurable: true,
});
const manager = new BookmarkManager();
expect(manager.browserType).toBe('chrome');
});
});Storage and GitHub Integration:
describe('End-to-End Sync Flow', () => {
it('should sync bookmark from storage to GitHub', async () => {
// Setup
const bookmark = createMockBookmark();
await storageManager.addBookmark(bookmark);
// Mock successful GitHub operations
mockOctokit.rest.repos.createOrUpdateFileContents
.mockResolvedValue({ data: { content: { sha: 'abc123' } } });
// Execute sync
const storedBookmarks = await storageManager.getBookmarks();
const markdown = generateMarkdownContent(storedBookmarks);
await github.createFile('bookmarks.md', markdown, 'Sync bookmarks');
// Verify
expect(mockOctokit.rest.repos.createOrUpdateFileContents)
.toHaveBeenCalledWith(expect.objectContaining({
path: 'bookmarks.md',
message: 'Sync bookmarks',
}));
});
});vitest.config.ts:
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'.output/',
'.wxt/',
'src/test/',
'**/*.test.{ts,tsx}',
'**/*.spec.{ts,tsx}',
]
}
},
});Test Setup File (src/test/setup.ts):
import '@testing-library/jest-dom';
// Global test utilities and mocks
global.browser = {
storage: {
sync: { get: vi.fn(), set: vi.fn(), clear: vi.fn() },
local: { get: vi.fn(), set: vi.fn(), clear: vi.fn() }
},
bookmarks: {
getTree: vi.fn(),
create: vi.fn(),
update: vi.fn(),
remove: vi.fn()
}
};graph LR
A[Code Push] --> B[Install Dependencies]
B --> C[Lint & Type Check]
C --> D[Unit Tests]
D --> E[Integration Tests]
E --> F[Build Extension]
F --> G[E2E Tests]
G --> H[Coverage Report]
# Run all tests
npm test
# Run specific test file
npm test utils/storage.test.ts
# Run with coverage
npm run test:coverage
# Watch mode for development
npm run test:watchGitHub API Responses:
const mockRepositoryResponse = {
id: 123,
name: 'test-repo',
full_name: 'user/test-repo',
description: 'Test repository',
private: true,
default_branch: 'main',
html_url: 'https://github.com/user/test-repo',
clone_url: 'https://github.com/user/test-repo.git',
};
mockOctokit.rest.repos.get.mockResolvedValue({
data: mockRepositoryResponse
});Storage Operations:
beforeEach(() => {
mockBrowser.storage.local.get.mockImplementation((key) => {
const storage = {
bookmarks: [],
idMappings: [],
lastSyncTime: 0,
};
return Promise.resolve({ [key]: storage[key] });
});
});Date Mocking:
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2023-12-07'));
});
afterEach(() => {
vi.useRealTimers();
});describe('Memory Usage', () => {
it('should not leak memory with large bookmark sets', async () => {
const initialMemory = process.memoryUsage().heapUsed;
// Process large bookmark set
const largeBookmarkSet = Array.from({ length: 10000 }, (_, i) =>
createMockBookmark({ id: `bookmark_${i}` })
);
await storageManager.saveBookmarks(largeBookmarkSet);
// Force garbage collection
if (global.gc) global.gc();
const finalMemory = process.memoryUsage().heapUsed;
const memoryIncrease = finalMemory - initialMemory;
// Should not increase by more than 50MB for 10k bookmarks
expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024);
});
});describe('Performance', () => {
it('should process bookmark changes efficiently', async () => {
const start = performance.now();
const changes = manager.detectChanges(oldBookmarks, newBookmarks);
const end = performance.now();
const processingTime = end - start;
// Should process 1000 bookmarks in under 100ms
expect(processingTime).toBeLessThan(100);
});
});- Visual Regression Tests: Screenshot comparisons for UI components
- Property-Based Tests: Generative testing for edge cases
- Mutation Testing: Validate test quality
- Performance Benchmarks: Automated performance regression detection
- Cross-Browser E2E: Automated testing in Chrome, Firefox, Safari
graph TB
A[GitHub PR] --> B[Trigger CI]
B --> C[Unit Tests]
B --> D[Lint/Type Check]
B --> E[Build Test]
C --> F{All Pass?}
D --> F
E --> F
F -->|Yes| G[Deploy Preview]
F -->|No| H[Block Merge]
G --> I[E2E Tests]
I --> J[Merge Ready]
This comprehensive testing strategy ensures HubMark maintains high quality, reliability, and cross-browser compatibility throughout development.