Unit testing/Bug fixing

This commit is contained in:
delphi 2025-03-16 21:17:19 +08:00
parent 4d8c156f4f
commit c057f10934
8 changed files with 4166 additions and 61 deletions

4
.gitignore vendored
View file

@ -20,3 +20,7 @@ data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
coverage/
test/

281
DEVELOPMENT_GUIDE.md Normal file
View file

@ -0,0 +1,281 @@
# Obsidian Cubox Plugin - Development Guide
## Overview
The Obsidian Cubox plugin allows users to sync articles and annotations from Cubox to Obsidian. This document provides a comprehensive guide to the project's architecture, components, and workflows to help developers quickly understand and contribute to the project.
## Project Architecture
The plugin is structured around several core components that work together:
```mermaid
graph TD
A[main.ts] --> B[cuboxApi.ts]
A --> C[templateProcessor.ts]
A --> D[cuboxSetting.ts]
B --> E[utils.ts]
C --> E
C --> F[templateInstructions.ts]
D --> G[Modal Components]
D --> F
G --> H[folderSelectModal.ts]
G --> I[statusSelectModal.ts]
G --> J[tagSelectModal.ts]
G --> K[typeSelectModal.ts]
G --> L[modalStyles.ts]
```
### Core Components
1. **Main Plugin (main.ts)**: Entry point that initializes the plugin, manages syncing, and coordinates between components
2. **API Client (cuboxApi.ts)**: Handles communication with the Cubox service
3. **Template Processor (templateProcessor.ts)**: Processes templates for note filename, metadata, and content
4. **Settings Manager (cuboxSetting.ts)**: Manages plugin settings and UI
5. **Modal Components (modal/*)**: Provides UI for selecting filters and options
6. **Utilities (utils.ts)**: Common utility functions
## Data Flow
```mermaid
sequenceDiagram
participant User
participant Plugin as CuboxSyncPlugin
participant API as CuboxApi
participant TP as TemplateProcessor
participant Vault as Obsidian Vault
User->>Plugin: Triggers sync
Plugin->>API: Request articles with filters
API-->>Plugin: Return article list
loop For each article
Plugin->>API: Get article details
API-->>Plugin: Return article content
Plugin->>TP: Process templates
TP-->>Plugin: Return formatted content
Plugin->>Vault: Create/update note file
end
Plugin-->>User: Display sync results
```
## Key Interfaces
### Cubox Data Structures
```typescript
// Article structure from Cubox API
interface CuboxArticle {
id: string;
title: string;
article_title: string;
description: string;
url: string;
domain: string;
create_time: string;
update_time: string;
word_count: number;
content?: string;
cubox_url: string;
highlights?: CuboxHighlight[];
tags?: string[];
type: string;
}
// Highlight structure from Cubox API
interface CuboxHighlight {
id: string;
text: string;
image_url?: string;
cubox_url: string;
note?: string;
color: string;
create_time: string;
}
// Folder structure from Cubox API
interface CuboxFolder {
id: string;
name: string;
nested_name: string;
uncategorized: boolean;
}
// Tag structure from Cubox API
interface CuboxTag {
id: string;
name: string;
nested_name: string;
parent_id: string | null;
}
```
### Plugin Settings
```typescript
// Main plugin settings
interface CuboxSyncSettings {
domain: string;
apiKey: string;
syncFrequency: number;
targetFolder: string;
filenameTemplate: string;
frontMatterVariables: string[];
contentTemplate: string;
dateFormat: string;
lastSyncTime: number;
lastSyncCardId: string;
lastCardUpdateTime: string;
folderFilter: string[];
typeFilter: string[];
statusFilter: string[];
tagsFilter: string[];
isRead: boolean;
isStarred: boolean;
isAnnotated: boolean;
syncing: boolean;
}
```
## Cubox API Integration
The plugin interacts with the Cubox API through the `CuboxApi` class. Key endpoints include:
1. **Card Filtering**: `/c/api/third-party/card/filter`
- Retrieves articles matching specified filters
- Supports pagination via `last_card_id` and `last_card_update_time`
2. **Content Retrieval**: `/c/api/third-party/card/content`
- Retrieves detailed content of a specific article
3. **Folder List**: `/c/api/third-party/group/list`
- Retrieves user's folder structure
4. **Tag List**: `/c/api/third-party/tag/list`
- Retrieves user's tag list
### Authentication
The API uses Bearer token authentication via the user's Cubox API key:
```typescript
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
}
```
## Template System
The plugin uses [Mustache.js](https://mustache.github.io/) for template rendering with a rich set of variables:
### Filename Template
Variables like `{{title}}`, `{{article_title}}`, `{{create_time}}`, etc.
### Front Matter (Metadata)
Supports all article properties with optional aliases (e.g., `title::customTitle`).
### Content Template
Rich template with support for article content, highlights, and metadata.
Special features:
- `{{content_highlighted}}`: Content with highlights marked using Obsidian's highlight syntax (`==highlighted text==`)
- Nested highlight properties: `{{text}}`, `{{note}}`, `{{color}}`, etc.
## Modal System
The plugin includes custom modal dialogs for selecting:
1. **Folders**: `folderSelectModal.ts`
2. **Status**: `statusSelectModal.ts` (read, starred, annotated)
3. **Tags**: `tagSelectModal.ts`
4. **Types**: `typeSelectModal.ts` (article, webpage, etc.)
These modals share a common styling approach defined in `modalStyles.ts` and provide a consistent UI for filter selection.
## Syncing Workflow
1. **Initialization**:
- Plugin loads settings and initializes API and template processor
- Sets up automatic sync interval if configured
2. **Sync Process**:
- Ensures target folder exists
- Fetches articles matching filters from Cubox
- For each article, retrieves content and processes templates
- Creates/updates files in the target folder
- Tracks last sync position for pagination
- Skips existing files based on Cubox ID in front matter
3. **Deduplication**:
- Files are identified by their Cubox ID stored in front matter
- Prevents duplicate imports of the same content
## Utilities
The `utils.ts` module provides helper functions for:
1. **File Safety**: Handling illegal characters in filenames
2. **Date Formatting**: Using Luxon for consistent date formatting
## Dependencies
- **[Obsidian API](https://github.com/obsidianmd/obsidian-api)**: Core API for Obsidian integration
- **[Mustache.js](https://mustache.github.io/)**: Template rendering
- **[Luxon](https://moment.github.io/luxon/)**: Date manipulation and formatting
## Development Workflow
1. Clone the repository
2. Install dependencies: `npm install`
3. Build the plugin: `npm run build`
4. For testing, copy or symlink the build output to your Obsidian plugins folder
## Extending the Plugin
### Adding New Filters
1. Create a new modal component in the `modal/` directory
2. Add appropriate settings in `cuboxSetting.ts`
3. Update API parameters in `cuboxApi.ts`
### Enhancing Templates
1. Add new variables to the template system in `templateProcessor.ts`
2. Update template instructions in `templateInstructions.ts`
3. Ensure proper documentation in the settings UI
## Common Patterns
### API Response Handling
```typescript
try {
const response = await this.request(path) as ApiResponse;
return response.data ?? [];
} catch (error) {
console.error('API request failed:', error);
throw error;
}
```
### Template Processing
```typescript
// Create view model with formatted data
const view = {
title: article.title || '',
create_time: formatDateTime(article.create_time, this.dateFormat),
// Additional properties...
};
// Render template with Mustache
const result = Mustache.render(template, view);
```
### Settings Management
```typescript
// Update setting
this.plugin.settings.someSetting = value;
await this.plugin.saveSettings();
// Apply setting change
this.plugin.updateComponent(value);
```

19
jest.config.js Normal file
View file

@ -0,0 +1,19 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleFileExtensions: ['ts', 'js'],
transform: {
'^.+\\.(ts|tsx)$': ['ts-jest', {
tsconfig: 'tsconfig.json',
}],
},
testMatch: ['**/test/**/*.test.ts'],
moduleNameMapper: {
// 模拟 Obsidian API
'obsidian': '<rootDir>/test/__mocks__/obsidian.ts',
},
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
}

3876
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,9 @@
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
"version": "node version-bump.mjs && git add manifest.json versions.json",
"test": "jest",
"test:watch": "jest --watch"
},
"keywords": [],
"author": "",
@ -23,7 +25,11 @@
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4",
"luxon": "^3.1.1"
"luxon": "^3.1.1",
"@types/jest": "^29.5.0",
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"jest-environment-jsdom": "^29.5.0"
},
"dependencies": {
"mustache": "^4.2.0",

View file

@ -10,7 +10,6 @@ export default class CuboxSyncPlugin extends Plugin {
cuboxApi: CuboxApi;
templateProcessor: TemplateProcessor;
syncIntervalId: number;
private statusBarItem: HTMLElement;
async onload() {
await this.loadSettings();
@ -30,10 +29,6 @@ export default class CuboxSyncPlugin extends Plugin {
});
ribbonIconEl.addClass('cubox-sync-ribbon-class');
// 添加状态栏
this.statusBarItem = this.addStatusBarItem();
this.statusBarItem.setText(`上次同步: ${this.formatLastSyncTime()}`);
// 添加同步命令
this.addCommand({
id: 'sync-cubox-data',
@ -95,9 +90,7 @@ export default class CuboxSyncPlugin extends Plugin {
// 设置同步状态为进行中
this.settings.syncing = true;
await this.saveSettings();
this.statusBarItem.setText(`正在同步 Cubox...`);
await this.ensureTargetFolder();
let lastCardId: string | null = this.settings.lastSyncCardId;
@ -211,15 +204,11 @@ export default class CuboxSyncPlugin extends Plugin {
this.settings.syncing = false;
await this.saveSettings();
// 更新状态栏
this.statusBarItem.setText(`上次同步: ${this.formatLastSyncTime()}`);
const message = `Cubox sync completed: ${syncCount} articles synchronized${skipCount > 0 ? `, ${skipCount} skipped` : ''}${errorCount > 0 ? `, ${errorCount} errors` : ''}`;
new Notice(message);
} catch (error) {
console.error('同步 Cubox 数据失败:', error);
new Notice('Cubox sync failed. Please check settings or network.');
this.statusBarItem.setText(`上次同步: ${this.formatLastSyncTime()} (失败)`);
} finally {
this.settings.syncing = false;
await this.saveSettings();

View file

@ -116,24 +116,26 @@ export class TemplateProcessor {
}
for (const item of templateVariables) {
const aliasedVariables = item.split('::')
const variable = aliasedVariables[0]
const aliasedVariables = item.split('::');
const variable = aliasedVariables[0];
const alias = aliasedVariables.length > 1 ? aliasedVariables[1] : variable;
if (
variable === 'tags' &&
article.tags &&
article.tags.length > 0
) {
frontMatter[variable] = article.tags
continue
frontMatter[alias] = article.tags;
continue;
}
const value = (article as any)[variable]
const value = (article as any)[variable];
if (value) {
frontMatter[variable] = value
frontMatter[alias] = value;
}
}
return stringifyYaml(frontMatter)
return stringifyYaml(frontMatter);
}
/**

View file

@ -43,9 +43,13 @@ export const formatDateTime = (dateString: string, format: string = 'yyyy-MM-dd
if (!dateString) return '';
try {
return DateTime.fromISO(dateString).toFormat(format);
const dt = DateTime.fromISO(dateString);
if (!dt.isValid) {
return dateString; // Return original string for invalid dates
}
return dt.toFormat(format);
} catch (error) {
console.error('Error formatting date:', error);
return dateString;
return dateString; // Return original string on error
}
};