chore: resolve conflict

This commit is contained in:
Quorafind 2025-07-09 16:58:46 +08:00
parent 7e5025f44d
commit 7061c2b58b
9 changed files with 412 additions and 66 deletions

View file

@ -17,6 +17,10 @@ import {
import { Task, CanvasTaskMetadata } from "../types/task";
import { createMockPlugin, createMockApp } from "./mockUtils";
// Mock Date to return consistent date for tests
const mockDate = new Date("2025-07-04T12:00:00.000Z");
const originalDate = Date;
// Mock Canvas task updater
const mockCanvasTaskUpdater = {
deleteCanvasTask: jest.fn(),

View file

@ -17,6 +17,10 @@ import { Task } from "../types/task";
import { createMockPlugin, createMockApp } from "./mockUtils";
import TaskProgressBarPlugin from "../index";
// Mock Date to return consistent date for tests
const mockDate = new Date("2025-07-04T12:00:00.000Z");
const originalDate = Date;
// Mock vault
const mockVault = {
getAbstractFileByPath: jest.fn(),
@ -44,6 +48,12 @@ describe("ArchiveActionExecutor - Markdown Tasks", () => {
mockPlugin = createMockPlugin();
mockApp = createMockApp();
// Mock Date globally
global.Date = jest.fn(() => mockDate) as any;
global.Date.now = jest.fn(() => mockDate.getTime());
global.Date.parse = originalDate.parse;
global.Date.UTC = originalDate.UTC;
// Reset mocks
jest.clearAllMocks();
@ -69,6 +79,11 @@ describe("ArchiveActionExecutor - Markdown Tasks", () => {
jest.restoreAllMocks();
});
afterEach(() => {
// Restore original Date
global.Date = originalDate;
});
describe("Markdown Task Archiving", () => {
it("should successfully archive Markdown task with onCompletion metadata cleanup", async () => {
const markdownTask: Task = {

View file

@ -50,7 +50,7 @@ describe("MoveActionExecutor", () => {
tags: [],
children: [],
},
line: 3,
line: 1,
filePath: "current.md",
};
@ -123,22 +123,18 @@ describe("MoveActionExecutor", () => {
it("should move task to existing target file", async () => {
const sourceContent = `# Current Tasks
- [ ] Keep this task
- [x] Task to move
- [ ] Keep this task too`;
const targetContent = `# Completed Tasks
- [x] Previous completed task`;
const expectedSourceContent = `# Current Tasks
- [ ] Keep this task
- [ ] Keep this task too`;
const expectedTargetContent = `# Completed Tasks
- [x] Previous completed task
- [x] Task to move`;
@ -158,14 +154,16 @@ describe("MoveActionExecutor", () => {
"Task moved to archive/completed.md successfully"
);
// Verify source file was updated (task removed)
expect(mockVault.modify).toHaveBeenCalledWith(
// Verify source file was updated (task removed) - first call
expect(mockVault.modify).toHaveBeenNthCalledWith(
1,
{ path: "current.md" },
expectedSourceContent
);
// Verify target file was updated (task added)
expect(mockVault.modify).toHaveBeenCalledWith(
// Verify target file was updated (task added) - second call
expect(mockVault.modify).toHaveBeenNthCalledWith(
2,
{ path: "archive/completed.md" },
expectedTargetContent
);
@ -174,7 +172,7 @@ describe("MoveActionExecutor", () => {
it("should create target file if it does not exist", async () => {
const taskWithCorrectLine = {
...mockTask,
line: 2, // Correct line for 3-line sourceContent
line: 0, // Correct line for single-line content
};
const contextWithCorrectLine = {
@ -182,19 +180,19 @@ describe("MoveActionExecutor", () => {
task: taskWithCorrectLine,
};
const sourceContent = `# Current Tasks
- [x] Task to move`;
const expectedSourceContent = `# Current Tasks`;
const sourceContent = `- [x] Task to move`;
const expectedSourceContent = ``;
// Target file is created empty, then modified with the task
const expectedTargetContent = `- [x] Task to move`;
// Mock source file operations
mockVault.getFileByPath
.mockReturnValueOnce({ path: "current.md" }) // Source file exists
.mockReturnValueOnce(null); // Target file doesn't exist
mockVault.read.mockResolvedValueOnce(sourceContent);
mockVault.read
.mockResolvedValueOnce(sourceContent) // Read source
.mockResolvedValueOnce(""); // Read target (empty after creation)
mockVault.create.mockResolvedValue({
path: "archive/completed.md",
});
@ -210,17 +208,25 @@ describe("MoveActionExecutor", () => {
"Task moved to archive/completed.md successfully"
);
// Verify target file was created with task
// Verify target file was created
expect(mockVault.create).toHaveBeenCalledWith(
"archive/completed.md",
expectedTargetContent
""
);
// Verify source file was updated
expect(mockVault.modify).toHaveBeenCalledWith(
// Verify source file was updated (task removed) - first call
expect(mockVault.modify).toHaveBeenNthCalledWith(
1,
{ path: "current.md" },
expectedSourceContent
);
// Verify target file was updated (task added) - second call
expect(mockVault.modify).toHaveBeenNthCalledWith(
2,
{ path: "archive/completed.md" },
expectedTargetContent
);
});
it("should move task to specific section in target file", async () => {
@ -232,7 +238,7 @@ describe("MoveActionExecutor", () => {
const taskWithCorrectLine = {
...mockTask,
line: 0, // Correct line for 1-line sourceContent
line: 0, // Correct line for single-line content
};
const contextWithCorrectLine = {
@ -263,6 +269,7 @@ describe("MoveActionExecutor", () => {
## Completed Tasks
- [x] Previous completed task
- [x] Task to move
## Other Section
- [ ] Some other task`;
@ -305,7 +312,7 @@ describe("MoveActionExecutor", () => {
const taskWithCorrectLine = {
...mockTask,
line: 0, // Correct line for 1-line sourceContent
line: 0, // Correct line for single-line content
};
const contextWithCorrectLine = {
@ -361,6 +368,17 @@ describe("MoveActionExecutor", () => {
});
it("should handle task not found in source file", async () => {
// Use a line number that's out of bounds
const taskWithInvalidLine = {
...mockTask,
line: 10, // Line doesn't exist in content
};
const contextWithInvalidLine = {
...mockContext,
task: taskWithInvalidLine,
};
const sourceContent = `# Current Tasks
- [ ] Different task
@ -371,7 +389,10 @@ describe("MoveActionExecutor", () => {
});
mockVault.read.mockResolvedValueOnce(sourceContent);
const result = await executor.execute(mockContext, config);
const result = await executor.execute(
contextWithInvalidLine,
config
);
expect(result.success).toBe(false);
expect(result.error).toBe("Task line not found in source file");
@ -389,7 +410,7 @@ describe("MoveActionExecutor", () => {
it("should handle target file creation error", async () => {
const taskWithCorrectLine = {
...mockTask,
line: 0, // Correct line for 1-line sourceContent
line: 0, // Correct line for single-line content
};
const contextWithCorrectLine = {
@ -420,7 +441,9 @@ describe("MoveActionExecutor", () => {
const taskWithMetadata = {
...mockTask,
content: "Task with metadata #tag @context 📅 2024-01-01",
line: 0, // Correct line for 1-line sourceContent
originalMarkdown:
"- [x] Task with metadata #tag @context 📅 2024-01-01",
line: 0, // Correct line for single-line content
};
const contextWithMetadata = {
@ -432,7 +455,6 @@ describe("MoveActionExecutor", () => {
const targetContent = `# Archive`;
const expectedSourceContent = ``; // Source file should be empty after task removal
const expectedTargetContent = `# Archive
- [x] Task with metadata #tag @context 📅 2024-01-01`;
mockVault.getFileByPath
@ -510,12 +532,26 @@ describe("MoveActionExecutor", () => {
targetFile: "archive.md",
};
// Task line 1 doesn't exist in empty file (only line 0 would be empty string)
const taskWithInvalidLine = {
...mockTask,
line: 1, // Line doesn't exist in empty content
};
const contextWithInvalidLine = {
...mockContext,
task: taskWithInvalidLine,
};
mockVault.getFileByPath.mockReturnValueOnce({
path: "current.md",
});
mockVault.read.mockResolvedValueOnce("");
const result = await executor.execute(mockContext, config);
const result = await executor.execute(
contextWithInvalidLine,
config
);
expect(result.success).toBe(false);
expect(result.error).toBe("Task line not found in source file");
@ -529,7 +565,7 @@ describe("MoveActionExecutor", () => {
const taskWithCorrectLine = {
...mockTask,
line: 0, // Correct line for 1-line sourceContent
line: 0, // Correct line for single-line content
};
const contextWithCorrectLine = {
@ -577,21 +613,28 @@ describe("MoveActionExecutor", () => {
targetFile: "archive.md",
};
const sourceContent = `# Project
const taskWithCorrectLine = {
...mockTask,
line: 2, // Correct line for the nested task
};
const contextWithCorrectLine = {
...mockContext,
task: taskWithCorrectLine,
};
const sourceContent = `# Project
- [ ] Parent task
- [x] Task to move
- [ ] Sibling task`;
const expectedSourceContent = `# Project
- [ ] Parent task
- [ ] Sibling task`;
const targetContent = `# Archive`;
const expectedTargetContent = `# Archive
- [x] Task to move`;
- [x] Task to move`;
mockVault.getFileByPath
.mockReturnValueOnce({ path: "current.md" })
@ -601,14 +644,23 @@ describe("MoveActionExecutor", () => {
.mockResolvedValueOnce(targetContent);
mockVault.modify.mockResolvedValue(undefined);
const result = await executor.execute(mockContext, config);
const result = await executor.execute(
contextWithCorrectLine,
config
);
expect(result.success).toBe(true);
expect(mockVault.modify).toHaveBeenCalledWith(
// Verify source file was updated (task removed) - first call
expect(mockVault.modify).toHaveBeenNthCalledWith(
1,
{ path: "current.md" },
expectedSourceContent
);
expect(mockVault.modify).toHaveBeenCalledWith(
// Verify target file was updated (task added) - second call
expect(mockVault.modify).toHaveBeenNthCalledWith(
2,
{ path: "archive.md" },
expectedTargetContent
);
@ -628,26 +680,15 @@ describe("MoveActionExecutor", () => {
tags: [],
children: [],
},
line: 2,
line: 0,
filePath: "source.md",
};
const sourceContent = `# Tasks
- [x] Task with onCompletion 🏁 delete
- [ ] Other task`;
const targetContent = `# Archive
- [x] Previous task`;
const expectedSourceContent = `# Tasks
- [ ] Other task`;
const sourceContent = `- [x] Task with onCompletion 🏁 delete`;
const targetContent = `# Archive`;
const expectedSourceContent = ``;
const expectedTargetContent = `# Archive
- [x] Previous task
- [x] Task with onCompletion`;
const config: OnCompletionMoveConfig = {
@ -674,14 +715,16 @@ describe("MoveActionExecutor", () => {
expect(result.success).toBe(true);
// Verify source file was updated (task removed)
expect(mockVault.modify).toHaveBeenCalledWith(
// Verify source file was updated (task removed) - first call
expect(mockVault.modify).toHaveBeenNthCalledWith(
1,
{ path: "source.md" },
expectedSourceContent
);
// Verify target file was updated (task added without onCompletion)
expect(mockVault.modify).toHaveBeenCalledWith(
// Verify target file was updated (task added without onCompletion) - second call
expect(mockVault.modify).toHaveBeenNthCalledWith(
2,
{ path: "archive.md" },
expectedTargetContent
);
@ -734,7 +777,17 @@ describe("MoveActionExecutor", () => {
const result = await executor.execute(context, config);
expect(result.success).toBe(true);
expect(mockVault.modify).toHaveBeenCalledWith(
// Verify source file was updated (task removed) - first call
expect(mockVault.modify).toHaveBeenNthCalledWith(
1,
{ path: "source.md" },
expectedSourceContent
);
// Verify target file was updated (task added without onCompletion) - second call
expect(mockVault.modify).toHaveBeenNthCalledWith(
2,
{ path: "archive.md" },
expectedTargetContent
);
@ -789,14 +842,16 @@ describe("MoveActionExecutor", () => {
expect(result.success).toBe(true);
// Verify source file was updated (task removed)
expect(mockVault.modify).toHaveBeenCalledWith(
// Verify source file was updated (task removed) - first call
expect(mockVault.modify).toHaveBeenNthCalledWith(
1,
{ path: "source.md" },
expectedSourceContent
);
// Verify target file was updated (task added without onCompletion)
expect(mockVault.modify).toHaveBeenCalledWith(
// Verify target file was updated (task added without onCompletion) - second call
expect(mockVault.modify).toHaveBeenNthCalledWith(
2,
{ path: "archive.md" },
expectedTargetContent
);

View file

@ -39,6 +39,7 @@ class MockTFolder {
class MockVault {
private files = new Map<string, MockTFile>();
private folders = new Map<string, MockTFolder>();
private fileContents = new Map<string, string>();
addFile(path: string, content: string): MockTFile {
@ -51,13 +52,19 @@ class MockVault {
addFolder(path: string): MockTFolder {
const folderName = path.split("/").pop() || "";
return new MockTFolder(path, folderName);
const folder = new MockTFolder(path, folderName);
this.folders.set(path, folder);
return folder;
}
getAbstractFileByPath(path: string): MockTFile | null {
return this.files.get(path) || null;
}
getFileByPath(path: string): MockTFile | null {
return this.files.get(path) || null;
}
async read(file: MockTFile): Promise<string> {
return this.fileContents.get(file.path) || "";
}
@ -152,7 +159,6 @@ describe("TaskParsingService Integration", () => {
stripExtension: true,
enabled: false,
},
enhancedProjectEnabled: true,
metadataConfigEnabled: true,
configFileEnabled: true,
},
@ -227,7 +233,20 @@ describe("TaskParsingService Integration", () => {
it("should parse tasks with metadata-based projects", async () => {
const parserConfig = createParserConfig();
const serviceOptions = createServiceOptions(parserConfig);
const serviceOptions = createServiceOptions(parserConfig, {
configFileName: "project.md",
searchRecursively: true,
metadataKey: "project",
pathMappings: [],
metadataMappings: [],
defaultProjectNaming: {
strategy: "filename",
stripExtension: true,
enabled: false,
},
metadataConfigEnabled: true,
configFileEnabled: true,
});
parsingService = new TaskParsingService(serviceOptions);
@ -261,7 +280,20 @@ describe("TaskParsingService Integration", () => {
it("should parse tasks with config file-based projects", async () => {
const parserConfig = createParserConfig();
const serviceOptions = createServiceOptions(parserConfig);
const serviceOptions = createServiceOptions(parserConfig, {
configFileName: "project.md",
searchRecursively: true,
metadataKey: "project",
pathMappings: [],
metadataMappings: [],
defaultProjectNaming: {
strategy: "filename",
stripExtension: true,
enabled: false,
},
metadataConfigEnabled: true,
configFileEnabled: true,
});
parsingService = new TaskParsingService(serviceOptions);
@ -575,6 +607,8 @@ describe("TaskParsingService Integration", () => {
stripExtension: true,
enabled: false,
},
metadataConfigEnabled: true,
configFileEnabled: true,
});
parsingService = new TaskParsingService(serviceOptions);
@ -622,6 +656,8 @@ describe("TaskParsingService Integration", () => {
stripExtension: true,
enabled: false,
},
metadataConfigEnabled: true,
configFileEnabled: true,
});
parsingService = new TaskParsingService(serviceOptions);
@ -671,7 +707,6 @@ describe("TaskParsingService Integration", () => {
stripExtension: true,
enabled: false,
},
enhancedProjectEnabled: true,
metadataConfigEnabled: true,
configFileEnabled: true,
});
@ -754,7 +789,6 @@ describe("TaskParsingService Integration", () => {
stripExtension: true,
enabled: false,
},
enhancedProjectEnabled: true,
metadataConfigEnabled: true,
configFileEnabled: true,
});
@ -840,6 +874,8 @@ describe("TaskParsingService Integration", () => {
stripExtension: true,
enabled: false,
},
metadataConfigEnabled: true,
configFileEnabled: true,
});
parsingService = new TaskParsingService(serviceOptions);
@ -901,6 +937,8 @@ describe("TaskParsingService Integration", () => {
stripExtension: true,
enabled: false,
},
metadataConfigEnabled: true,
configFileEnabled: true,
});
parsingService = new TaskParsingService(serviceOptions);
@ -952,6 +990,8 @@ describe("TaskParsingService Integration", () => {
stripExtension: true,
enabled: true,
},
metadataConfigEnabled: true,
configFileEnabled: true,
});
parsingService = new TaskParsingService(serviceOptions);
@ -1013,6 +1053,8 @@ describe("TaskParsingService Integration", () => {
stripExtension: true,
enabled: false,
},
metadataConfigEnabled: true,
configFileEnabled: true,
});
parsingService = new TaskParsingService(serviceOptions);
@ -1064,7 +1106,6 @@ describe("TaskParsingService Integration", () => {
stripExtension: true,
enabled: true,
},
enhancedProjectEnabled: true,
metadataConfigEnabled: true,
configFileEnabled: true,
});

View file

@ -15,6 +15,7 @@ import {
} from "../editor-ext/autoCompleteParent"; // Adjust the import path as necessary
import { TaskProgressBarSettings } from "../common/setting-definition";
import { EditorView } from "@codemirror/view";
import { Task } from "../types/task";
const mockAnnotationType = {
of: jest.fn().mockImplementation((value: string) => ({
@ -658,6 +659,27 @@ export function createMockCanvasTaskUpdater() {
};
}
/**
* Create a mock Task object with all required fields
*/
export function createMockTask(overrides: Partial<Task> = {}): Task {
return {
id: "test-task-id",
content: "Test task content",
completed: false,
status: " ",
metadata: {
tags: [],
children: [],
...overrides.metadata,
},
filePath: "test.md",
line: 1,
originalMarkdown: "- [ ] Test task content",
...overrides,
};
}
export {
// createMockText is already exported inline
createMockChangeSet, // Export the consolidated function

View file

@ -79,6 +79,7 @@ export abstract class BaseTaskBasesView extends Component implements BasesView {
protected plugin: TaskProgressBarPlugin;
protected tasks: Task[] = [];
protected viewMode: ViewMode;
protected currentTask: Task | null = null;
// Details panel properties
protected detailsComponent: TaskDetailsComponent;

View file

@ -508,7 +508,199 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
}
private displayTimeParsingSettings(containerEl: HTMLElement): void {
<<<<<<< HEAD
renderTimeParsingSettingsTab(this, containerEl);
=======
containerEl.createEl("h2", { text: t("Time Parsing Settings") });
// Enable Time Parsing
new Setting(containerEl)
.setName(t("Enable Time Parsing"))
.setDesc(
t(
"Automatically parse natural language time expressions in Quick Capture"
)
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.timeParsing?.enabled ?? true)
.onChange(async (value) => {
if (!this.plugin.settings.timeParsing) {
this.plugin.settings.timeParsing = {
enabled: value,
supportedLanguages: ["en", "zh"],
dateKeywords: {
start: [
"start",
"begin",
"from",
"开始",
"从",
],
due: [
"due",
"deadline",
"by",
"until",
"截止",
"到期",
"之前",
],
scheduled: [
"scheduled",
"on",
"at",
"安排",
"计划",
"在",
],
},
removeOriginalText: true,
perLineProcessing: false,
realTimeReplacement: false,
};
} else {
this.plugin.settings.timeParsing.enabled = value;
}
this.applySettingsUpdate();
})
);
// Remove Original Text
new Setting(containerEl)
.setName(t("Remove Original Time Expressions"))
.setDesc(t("Remove parsed time expressions from the task text"))
.addToggle((toggle) =>
toggle
.setValue(
this.plugin.settings.timeParsing?.removeOriginalText ??
true
)
.onChange(async (value) => {
if (!this.plugin.settings.timeParsing) return;
this.plugin.settings.timeParsing.removeOriginalText =
value;
this.applySettingsUpdate();
})
);
// Supported Languages
containerEl.createEl("h3", { text: t("Supported Languages") });
containerEl.createEl("p", {
text: t(
"Currently supports English and Chinese time expressions. More languages may be added in future updates."
),
cls: "setting-item-description",
});
// Date Keywords Configuration
containerEl.createEl("h3", { text: t("Date Keywords Configuration") });
// Start Date Keywords
new Setting(containerEl)
.setName(t("Start Date Keywords"))
.setDesc(t("Keywords that indicate start dates (comma-separated)"))
.addTextArea((text) => {
const keywords =
this.plugin.settings.timeParsing?.dateKeywords?.start || [];
text.setValue(keywords.join(", "))
.setPlaceholder("start, begin, from, 开始, 从")
.onChange(async (value) => {
if (!this.plugin.settings.timeParsing) return;
this.plugin.settings.timeParsing.dateKeywords.start =
value
.split(",")
.map((k) => k.trim())
.filter((k) => k.length > 0);
this.applySettingsUpdate();
});
text.inputEl.rows = 2;
});
// Due Date Keywords
new Setting(containerEl)
.setName(t("Due Date Keywords"))
.setDesc(t("Keywords that indicate due dates (comma-separated)"))
.addTextArea((text) => {
const keywords =
this.plugin.settings.timeParsing?.dateKeywords?.due || [];
text.setValue(keywords.join(", "))
.setPlaceholder(
"due, deadline, by, until, 截止, 到期, 之前"
)
.onChange(async (value) => {
if (!this.plugin.settings.timeParsing) return;
this.plugin.settings.timeParsing.dateKeywords.due =
value
.split(",")
.map((k) => k.trim())
.filter((k) => k.length > 0);
this.applySettingsUpdate();
});
text.inputEl.rows = 2;
});
// Scheduled Date Keywords
new Setting(containerEl)
.setName(t("Scheduled Date Keywords"))
.setDesc(
t("Keywords that indicate scheduled dates (comma-separated)")
)
.addTextArea((text) => {
const keywords =
this.plugin.settings.timeParsing?.dateKeywords?.scheduled ||
[];
text.setValue(keywords.join(", "))
.setPlaceholder("scheduled, on, at, 安排, 计划, 在")
.onChange(async (value) => {
if (!this.plugin.settings.timeParsing) return;
this.plugin.settings.timeParsing.dateKeywords.scheduled =
value
.split(",")
.map((k) => k.trim())
.filter((k) => k.length > 0);
this.applySettingsUpdate();
});
text.inputEl.rows = 2;
});
// Examples
containerEl.createEl("h3", { text: t("Examples") });
const examplesEl = containerEl.createEl("div", {
cls: "time-parsing-examples",
});
const examples = [
{ input: "go to bed tomorrow", output: "go to bed 📅 2025-01-05" },
{ input: "meeting next week", output: "meeting 📅 2025-01-11" },
{ input: "project due by Friday", output: "project 📅 2025-01-04" },
{ input: "明天开会", output: "开会 📅 2025-01-05" },
{ input: "3天后完成", output: "完成 📅 2025-01-07" },
];
examples.forEach((example) => {
const exampleEl = examplesEl.createEl("div", {
cls: "time-parsing-example",
});
exampleEl.createEl("span", {
text: "Input: ",
cls: "example-label",
});
exampleEl.createEl("code", {
text: example.input,
cls: "example-input",
});
exampleEl.createEl("br");
exampleEl.createEl("span", {
text: "Output: ",
cls: "example-label",
});
exampleEl.createEl("code", {
text: example.output,
cls: "example-output",
});
});
>>>>>>> ead40d2 (fix: tests issue)
}
private displayTimelineSidebarSettings(containerEl: HTMLElement): void {

View file

@ -4,6 +4,15 @@
import './__mocks__/dom-helpers';
// Add Jest types
declare global {
namespace jest {
interface Mock<T = any, Y extends any[] = any[]> {
(...args: Y): T;
}
}
}
// jsdom is already set up by jest-environment-jsdom
// Just need to ensure our DOM helpers are loaded

View file

@ -32,6 +32,13 @@ export class CompleteActionExecutor extends BaseActionExecutor {
return this.createErrorResult("Invalid complete configuration");
}
return this.executeCommon(context, config);
}
private async executeCommon(
context: OnCompletionExecutionContext,
config: OnCompletionConfig
): Promise<OnCompletionExecutionResult> {
const completeConfig = config as OnCompletionCompleteConfig;
const { plugin } = context;