test: add initial test suite for plugin functionality

- Add Jest configuration and TypeScript support
- Create tests for main plugin functionality (initialization, settings)
- Add tests for Asana API integration (workspaces, projects, tasks)
- Set up mock files for Obsidian API and test environment
- Add test scripts to package.json

This commit sets up a basic test infrastructure to ensure the plugin's
core functionality works as expected and to catch potential regressions
in future changes.
This commit is contained in:
Ryan Bantz 2025-04-29 17:03:43 -05:00
parent e80e621e56
commit b425a7b952
7 changed files with 4187 additions and 5 deletions

21
jest.config.js Normal file
View file

@ -0,0 +1,21 @@
module.exports = {
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: 'tsconfig.json'
}]
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'^obsidian$': '<rootDir>/tests/__mocks__/obsidian.js'
},
setupFiles: ['<rootDir>/tests/setup.js'],
transformIgnorePatterns: [
'node_modules/(?!(obsidian)/)'
],
testPathIgnorePatterns: [
'/node_modules/',
'/dist/'
]
};

3827
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,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 --config jest.config.js",
"test:watch": "jest --watch --config jest.config.js"
},
"keywords": [],
"author": "Ryan Bantz",
@ -13,7 +15,9 @@
"devDependencies": {
"builtin-modules": "^4.0.0",
"esbuild": "0.17.3",
"jest": "^29.7.0",
"obsidian": "^1.7.2",
"ts-jest": "^29.1.1",
"tslib": "^2.8.1",
"typescript": "^4.0.0"
}

115
tests/__mocks__/obsidian.js Normal file
View file

@ -0,0 +1,115 @@
class App {
constructor() {
this.workspace = {
activeLeaf: {
view: {
editor: {
getCursor: () => ({ line: 0, ch: 0 }),
getLine: () => '',
replaceRange: jest.fn()
}
}
}
};
}
}
class Editor {
constructor() {
this.getCursor = jest.fn();
this.getLine = jest.fn();
this.replaceRange = jest.fn();
}
}
class MarkdownView {
constructor() {
this.editor = new Editor();
}
}
class Notice {
constructor(message) {
this.message = message;
}
}
class Plugin {
constructor(app, manifest) {
this.app = app;
this.manifest = manifest;
}
loadData() {
return Promise.resolve({});
}
saveData(data) {
return Promise.resolve();
}
}
class PluginSettingTab {
constructor(app, plugin) {
this.app = app;
this.plugin = plugin;
}
}
class Setting {
constructor(containerEl) {
this.containerEl = containerEl;
}
setName(name) {
this.name = name;
return this;
}
setDesc(desc) {
this.desc = desc;
return this;
}
addText(callback) {
callback({ setValue: jest.fn(), onChange: jest.fn() });
return this;
}
addToggle(callback) {
callback({ setValue: jest.fn(), onChange: jest.fn() });
return this;
}
addButton(callback) {
callback({ setButtonText: jest.fn(), onClick: jest.fn() });
return this;
}
}
class FuzzySuggestModal {
constructor(app, items, resolve) {
this.app = app;
this.items = items;
this.resolve = resolve;
}
onOpen() {}
getItems() { return this.items; }
getItemText(item) { return item.name; }
onChooseItem(item) { this.resolve(item); }
}
const requestUrl = jest.fn();
module.exports = {
App,
Editor,
MarkdownView,
Notice,
Plugin,
PluginSettingTab,
Setting,
FuzzySuggestModal,
requestUrl
};

155
tests/api.test.js Normal file
View file

@ -0,0 +1,155 @@
const { requestUrl } = require('obsidian');
const {
fetchAsanaWorkspaces,
fetchAsanaProjects,
fetchAsanaSections,
createTaskInAsana
} = require('../src/api/asanaApi');
jest.mock('obsidian', () => ({
requestUrl: jest.fn()
}));
describe('Asana API', () => {
const mockSettings = {
asanaToken: 'test-token',
showArchivedProjects: false
};
beforeEach(() => {
jest.clearAllMocks();
});
test('fetchAsanaWorkspaces handles successful response', async () => {
const mockWorkspaces = [{ id: '1', name: 'Workspace 1' }];
requestUrl.mockResolvedValueOnce({
status: 200,
json: { data: mockWorkspaces }
});
const result = await fetchAsanaWorkspaces(mockSettings);
expect(result).toEqual(mockWorkspaces);
expect(requestUrl).toHaveBeenCalledWith({
url: 'https://app.asana.com/api/1.0/workspaces',
method: 'GET',
headers: {
Authorization: 'Bearer test-token'
}
});
});
test('fetchAsanaWorkspaces handles error response', async () => {
requestUrl.mockRejectedValueOnce(new Error('API Error'));
await expect(fetchAsanaWorkspaces(mockSettings)).rejects.toThrow('Failed to retrieve workspaces from Asana');
});
test('fetchAsanaProjects handles successful response', async () => {
const mockProjects = [{ id: '1', name: 'Project 1' }];
requestUrl.mockResolvedValueOnce({
status: 200,
json: { data: mockProjects }
});
const result = await fetchAsanaProjects('workspace-1', mockSettings);
expect(result).toEqual(mockProjects);
expect(requestUrl).toHaveBeenCalledWith({
url: 'https://app.asana.com/api/1.0/workspaces/workspace-1/projects?is_archived=false',
method: 'GET',
headers: {
Authorization: 'Bearer test-token'
}
});
});
test('createTaskInAsana handles successful response', async () => {
// Mock initial task creation
const mockTaskGid = '1234';
requestUrl.mockResolvedValueOnce({
status: 201,
json: { data: { gid: mockTaskGid, name: 'Test Task' } }
});
// Mock section addition
requestUrl.mockResolvedValueOnce({
status: 200,
json: { data: { gid: mockTaskGid } }
});
// Mock task details fetch
const mockTaskDetails = {
gid: mockTaskGid,
name: 'Test Task',
permalink_url: 'https://app.asana.com/0/1/1'
};
requestUrl.mockResolvedValueOnce({
status: 200,
json: { data: mockTaskDetails }
});
const result = await createTaskInAsana(
'Test Task',
'workspace-1',
'project-1',
'section-1',
mockSettings
);
expect(result).toEqual(mockTaskDetails);
expect(requestUrl).toHaveBeenCalledTimes(3);
// Verify task creation call
expect(requestUrl).toHaveBeenNthCalledWith(1, {
url: 'https://app.asana.com/api/1.0/tasks',
method: 'POST',
headers: {
Authorization: 'Bearer test-token',
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: {
name: 'Test Task',
workspace: 'workspace-1',
projects: ['project-1'],
assignee: undefined
}
})
});
// Verify section addition call
expect(requestUrl).toHaveBeenNthCalledWith(2, {
url: 'https://app.asana.com/api/1.0/sections/section-1/addTask',
method: 'POST',
headers: {
Authorization: 'Bearer test-token',
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: {
task: mockTaskGid
}
})
});
// Verify task details fetch call
expect(requestUrl).toHaveBeenNthCalledWith(3, {
url: `https://app.asana.com/api/1.0/tasks/${mockTaskGid}`,
method: 'GET',
headers: {
Authorization: 'Bearer test-token'
}
});
});
test('createTaskInAsana handles error response', async () => {
requestUrl.mockRejectedValueOnce(new Error('API Error'));
await expect(createTaskInAsana(
'Test Task',
'workspace-1',
'project-1',
'section-1',
mockSettings
)).rejects.toThrow('Failed to create task in Asana');
});
});

52
tests/main.test.js Normal file
View file

@ -0,0 +1,52 @@
const { App, Plugin } = require('obsidian');
const AsanaPlugin = require('../main').default;
describe('Asana Plugin', () => {
let plugin;
let app;
beforeEach(async () => {
app = new App();
plugin = new AsanaPlugin(app, {});
await plugin.loadSettings();
});
afterEach(() => {
if (plugin.onunload) {
plugin.onunload();
}
});
test('Plugin loads successfully', () => {
expect(plugin).toBeDefined();
expect(plugin.settings).toBeDefined();
});
test('Default settings are set correctly', () => {
expect(plugin.settings.asanaToken).toBe('');
expect(plugin.settings.markTaskAsCompleted).toBe(false);
expect(plugin.settings.pinnedProjects).toEqual([]);
expect(plugin.settings.enableMarkdownLink).toBe(true);
expect(plugin.settings.showArchivedProjects).toBe(false);
expect(plugin.settings.pinMyTasks).toBe(true);
});
test('Settings can be saved and loaded', async () => {
const testSettings = {
asanaToken: 'test-token',
markTaskAsCompleted: true,
pinnedProjects: ['Project 1', 'Project 2'],
enableMarkdownLink: false,
showArchivedProjects: true,
pinMyTasks: false
};
// Mock the loadData method to return our test settings
plugin.loadData = jest.fn().mockResolvedValue(testSettings);
// Load the settings
await plugin.loadSettings();
expect(plugin.settings).toEqual(testSettings);
});
});

16
tests/setup.js Normal file
View file

@ -0,0 +1,16 @@
// Mock the global window object
global.window = {
document: {
createElement: jest.fn(),
createDocumentFragment: jest.fn()
}
};
// Mock the global document object
global.document = {
createElement: jest.fn(),
createDocumentFragment: jest.fn()
};
// Mock console.error to prevent test output pollution
console.error = jest.fn();