add prettier

This commit is contained in:
Mike Rodarte 2025-03-10 23:56:14 -04:00
parent 820e71b119
commit ee94db5e83
No known key found for this signature in database
GPG key ID: 32DCD8C789A592CC
27 changed files with 476 additions and 239 deletions

View file

@ -3,10 +3,10 @@ name: Word Frequency Pipeline
on:
push:
branches:
- '*'
- "*"
pull_request:
branches:
- '*'
- "*"
jobs:
build_test:
@ -19,11 +19,14 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '23'
node-version: "23"
- name: Install dependencies
run: npm install
- name: Run Prettier
run: npx prettier --check
- name: Lint code
run: npm run lint
@ -54,7 +57,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '23'
node-version: "23"
- name: Install dependencies
run: npm install

3
.prettierignore Normal file
View file

@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage

7
.prettierrc Normal file
View file

@ -0,0 +1,7 @@
{
"bracketSameLine": true,
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "es5"
}

View file

@ -1,2 +1,3 @@
# obsidian-word-frequency
Count word frequency in current note within Obsidian

View file

@ -1,55 +1,55 @@
class MockMarkdownView {
editor = {
getValue: jest.fn(),
};
containerEl = {
addEventListener: jest.fn(),
};
getViewType = jest.fn();
getViewData = jest.fn();
onunload = jest.fn();
getDisplayText = jest.fn();
constructor() { }
editor = {
getValue: jest.fn(),
};
containerEl = {
addEventListener: jest.fn(),
};
getViewType = jest.fn();
getViewData = jest.fn();
onunload = jest.fn();
getDisplayText = jest.fn();
constructor() {}
}
module.exports = {
setIcon: jest.fn(),
App: jest.fn(),
Plugin: jest.fn(),
MarkdownView: MockMarkdownView,
WorkspaceLeaf: jest.fn(),
Editor: jest.fn(),
PluginSettingTab: jest.fn(),
ItemView: jest.fn(),
CustomEvent: jest.fn(),
Setting: jest.fn().mockImplementation((containerEl: HTMLElement) => {
const settingEl = document.createElement('div');
const textArea = document.createElement('textarea');
textArea.classList.add('word-frequency-setting-blacklist');
settingEl.appendChild(textArea);
containerEl.appendChild(settingEl);
setIcon: jest.fn(),
App: jest.fn(),
Plugin: jest.fn(),
MarkdownView: MockMarkdownView,
WorkspaceLeaf: jest.fn(),
Editor: jest.fn(),
PluginSettingTab: jest.fn(),
ItemView: jest.fn(),
CustomEvent: jest.fn(),
Setting: jest.fn().mockImplementation((containerEl: HTMLElement) => {
const settingEl = document.createElement("div");
const textArea = document.createElement("textarea");
textArea.classList.add("word-frequency-setting-blacklist");
settingEl.appendChild(textArea);
containerEl.appendChild(settingEl);
return {
setName: jest.fn().mockReturnThis(),
setDesc: jest.fn().mockReturnThis(),
addTextArea: jest.fn().mockImplementation((callback) => {
callback({
setValue: function (value: string) {
textArea.value = value;
return this;
},
onChange: function (handler: (value: string) => void) {
textArea.addEventListener("change", (event) => {
handler((event.target as HTMLTextAreaElement).value);
});
return this;
},
inputEl: textArea,
});
return {
setName: jest.fn().mockReturnThis(),
setDesc: jest.fn().mockReturnThis(),
addTextArea: jest.fn().mockImplementation((callback) => {
callback({
setValue: function (value: string) {
textArea.value = value;
return this;
},
onChange: function (handler: (value: string) => void) {
textArea.addEventListener('change', (event) => {
handler((event.target as HTMLTextAreaElement).value);
});
return this;
},
inputEl: textArea,
});
return {
inputEl: textArea,
};
}),
inputEl: textArea,
};
}),
};
}),
};
}),
};

View file

@ -1,3 +1,3 @@
export const debounce = jest.fn((func) => {
return jest.fn(func);
});
return jest.fn(func);
});

View file

@ -1,31 +1,28 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginJs from '@eslint/js';
import prettierConfig from 'eslint-config-prettier';
import globals from 'globals';
import tseslint from 'typescript-eslint';
/** @type {import('eslint').Linter.Config[]} */
export default [
{
files: ['**/*.{js,mjs,cjs,ts}']
files: ['**/*.{js,mjs,cjs,ts}'],
},
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
...globals.node,
},
parserOptions: {
sourceType: 'module'
}
}
sourceType: 'module',
},
},
},
{
ignores: [
'__mocks__/**',
'coverage/**',
'dist/**',
'node_modules/**',
],
},
pluginJs.configs.recommended,
{
ignores: ['__mocks__/**', 'coverage/**', 'dist/**', 'node_modules/**'],
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
prettierConfig,
];

View file

@ -1,20 +1,17 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
modulePathIgnorePatterns: ['<rootDir>/dist/'],
testMatch: ['<rootDir>/src/__tests__/**/*.test.ts'],
coverageDirectory: 'coverage',
coverageReporters: ['cobertura', 'html', 'text', 'text-summary'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
],
coverageThreshold: {
global: {
branches: 90,
functions: 70,
lines: 90,
statements: 90,
},
preset: "ts-jest",
testEnvironment: "jsdom",
modulePathIgnorePatterns: ["<rootDir>/dist/"],
testMatch: ["<rootDir>/src/__tests__/**/*.test.ts"],
coverageDirectory: "coverage",
coverageReporters: ["cobertura", "html", "text", "text-summary"],
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],
coverageThreshold: {
global: {
branches: 90,
functions: 70,
lines: 90,
statements: 90,
},
},
};

42
package-lock.json generated
View file

@ -20,6 +20,7 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"globals": "^16.0.0",
@ -27,6 +28,7 @@
"jest-environment-jsdom": "^29.7.0",
"jsdom": "^26.0.0",
"obsidian": "^1.8.7",
"prettier": "^3.5.3",
"rollup": "^4.34.9",
"rollup-plugin-typescript2": "^0.36.0",
"ts-jest": "^29.2.6",
@ -3471,6 +3473,18 @@
}
}
},
"node_modules/eslint-config-prettier": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz",
"integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-import-resolver-node": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
@ -6616,6 +6630,21 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@ -10534,6 +10563,13 @@
}
}
},
"eslint-config-prettier": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz",
"integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==",
"dev": true,
"requires": {}
},
"eslint-import-resolver-node": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
@ -12791,6 +12827,12 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
"prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true
},
"pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",

View file

@ -11,7 +11,9 @@
"build": "rollup -c",
"dev": "npm run build && obsidian --dev",
"watch": "rollup -c --watch",
"pretty-fix": "npx prettier src --write",
"lint": "eslint . --ext .js,.ts",
"lint-fix": "eslint . --ext .js,.ts --fix",
"test": "jest",
"coverage": "npx jest --coverage"
},
@ -38,6 +40,7 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"globals": "^16.0.0",
@ -45,6 +48,7 @@
"jest-environment-jsdom": "^29.7.0",
"jsdom": "^26.0.0",
"obsidian": "^1.8.7",
"prettier": "^3.5.3",
"rollup": "^4.34.9",
"rollup-plugin-typescript2": "^0.36.0",
"ts-jest": "^29.2.6",

View file

@ -1,19 +1,14 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import json from '@rollup/plugin-json';
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import json from "@rollup/plugin-json";
export default {
input: 'src/main.ts',
output: {
file: 'dist/main.js',
format: 'cjs',
},
plugins: [
commonjs(),
json(),
resolve(),
typescript()
],
external: ['obsidian'],
input: "src/main.ts",
output: {
file: "dist/main.js",
format: "cjs",
},
plugins: [commonjs(), json(), resolve(), typescript()],
external: ["obsidian"],
};

View file

@ -8,7 +8,10 @@ export class ViewManager {
this.plugin = plugin;
}
getOrCreateLeaf(workspace: Workspace, viewType: string): WorkspaceLeaf | null {
getOrCreateLeaf(
workspace: Workspace,
viewType: string
): WorkspaceLeaf | null {
const leaves = workspace.getLeavesOfType(viewType);
if (leaves.length > 0) {
return leaves[0];
@ -20,12 +23,13 @@ export class ViewManager {
async setViewState(leaf: WorkspaceLeaf, viewType: string): Promise<void> {
await leaf.setViewState({
type: viewType,
active: true
active: true,
});
}
updateContent(): void {
const editor = this.plugin.app.workspace.getActiveViewOfType(MarkdownView)?.editor;
const editor =
this.plugin.app.workspace.getActiveViewOfType(MarkdownView)?.editor;
this.plugin.frequencyCounter.triggerUpdateContent(editor);
}
}

View file

@ -25,7 +25,10 @@ export class WordFrequencyCounter {
return Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]);
}
handleActiveLeafChange(leaf: WorkspaceLeaf | null, workspace: Workspace): void {
handleActiveLeafChange(
leaf: WorkspaceLeaf | null,
workspace: Workspace
): void {
if (leaf === null) {
return;
}
@ -64,7 +67,9 @@ export class WordFrequencyCounter {
}
try {
const wordCounts = this.calculateWordFrequencies(editor.getValue());
window.document.dispatchEvent(new CustomEvent(EVENT_UPDATE, { detail: { wordCounts } }));
window.document.dispatchEvent(
new CustomEvent(EVENT_UPDATE, { detail: { wordCounts } })
);
} catch (error) {
console.error('error in triggerUpdateContent', error);
}

View file

@ -12,18 +12,27 @@ export class WordFrequencyDisplay {
this.view = view;
}
addWordToSidebar(blacklist: Set<string>, word: string, count: number, contentContainer: HTMLDivElement) {
addWordToSidebar(
blacklist: Set<string>,
word: string,
count: number,
contentContainer: HTMLDivElement
) {
if (blacklist.has(word) || count < this.plugin.settings.threshold) {
return;
}
const row = contentContainer.createEl('div', { cls: 'word-row' });
const wordCountContainer = row.createEl('div', { cls: 'word-count-container' });
const wordCountContainer = row.createEl('div', {
cls: 'word-count-container',
});
wordCountContainer.createEl('span', { text: word });
wordCountContainer.createEl('span', { text: count.toString() });
const buttonContainer = row.createEl('div', { cls: 'button-container' });
const buttonContainer = row.createEl('div', {
cls: 'button-container',
});
const button = buttonContainer.createEl('button');
setIcon(button, 'trash-2');
button.addEventListener('click', () => {
@ -38,9 +47,16 @@ export class WordFrequencyDisplay {
}
createThresholdDisplay(contentEl: HTMLElement) {
const thresholdDisplay = contentEl.createEl('div', { cls: 'threshold-display' });
thresholdDisplay.setText(`Current Frequency Threshold is ${this.plugin.settings.threshold}.`);
thresholdDisplay.setAttr('title', 'Configure settings for this plugin to update the frequency threshold.');
const thresholdDisplay = contentEl.createEl('div', {
cls: 'threshold-display',
});
thresholdDisplay.setText(
`Current Frequency Threshold is ${this.plugin.settings.threshold}.`
);
thresholdDisplay.setAttr(
'title',
'Configure settings for this plugin to update the frequency threshold.'
);
}
saveWordToBlacklist(word: string) {

View file

@ -20,26 +20,24 @@ export class WordFrequencySettingTab extends PluginSettingTab {
.setName('Blacklist')
.setDesc('Comma-separated list of words to exclude.')
.addTextArea((text) => {
text
.setValue(this.plugin.settings.blacklist)
text.setValue(this.plugin.settings.blacklist)
.onChange(async (value) => {
await this.saveBlacklistValue(value);
})
.inputEl.classList.add('word-frequency-setting-blacklist')
}
);
.inputEl.classList.add('word-frequency-setting-blacklist');
});
new Setting(containerEl)
.setName('Word Frequency Threshold')
.setDesc('Only show words that appear at least this many times.')
.addText(text => text
.setPlaceholder('3')
.setValue(this.plugin.settings.threshold.toString())
.onChange(async (value) => {
await this.updateThreshold(value);
}
)
);
.addText((text) =>
text
.setPlaceholder('3')
.setValue(this.plugin.settings.threshold.toString())
.onChange(async (value) => {
await this.updateThreshold(value);
})
);
}
async saveBlacklistValue(value: string) {
@ -55,7 +53,7 @@ export class WordFrequencySettingTab extends PluginSettingTab {
this.plugin.settings.threshold = num;
await this.plugin.saveSettings();
this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE).forEach(leaf => {
this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE).forEach((leaf) => {
(leaf.view as WordFrequencyView).updateContent();
});
}

View file

@ -1,16 +1,24 @@
import { ItemView, WorkspaceLeaf } from 'obsidian';
import { EVENT_UPDATE, FREQUENCY_ICON, PLUGIN_NAME, VIEW_TYPE } from './constants';
import {
EVENT_UPDATE,
FREQUENCY_ICON,
PLUGIN_NAME,
VIEW_TYPE,
} from './constants';
import WordFrequencyPlugin from './main';
import { WordFrequencyDisplay } from './WordFrequencyDisplay';
export class WordFrequencyView extends ItemView {
private display: WordFrequencyDisplay;
private eventListener: (event: CustomEvent) => void = () => {
};
private eventListener: (event: CustomEvent) => void = () => {};
private readonly plugin: WordFrequencyPlugin;
private wordCountList: [string, number][] = [];
constructor(leaf: WorkspaceLeaf, plugin: WordFrequencyPlugin, display?: WordFrequencyDisplay) {
constructor(
leaf: WorkspaceLeaf,
plugin: WordFrequencyPlugin,
display?: WordFrequencyDisplay
) {
super(leaf);
this.plugin = plugin;
this.display = display ?? new WordFrequencyDisplay(plugin, this);
@ -39,23 +47,36 @@ export class WordFrequencyView extends ItemView {
this.updateContent();
}
};
window.document.addEventListener(EVENT_UPDATE, this.eventListener as EventListener);
window.document.addEventListener(
EVENT_UPDATE,
this.eventListener as EventListener
);
this.updateContent();
}
async onClose() {
window.document.removeEventListener(EVENT_UPDATE, this.eventListener as EventListener);
window.document.removeEventListener(
EVENT_UPDATE,
this.eventListener as EventListener
);
}
updateContent() {
this.contentEl.empty();
this.display.createHeader(this.contentEl);
const contentContainer = this.contentEl.createEl('div');
const blacklist = new Set(this.plugin.settings.blacklist.split(',').map(word => word.trim()));
const blacklist = new Set(
this.plugin.settings.blacklist.split(',').map((word) => word.trim())
);
this.wordCountList.forEach(([word, count]) => {
this.display.addWordToSidebar(blacklist, word, count, contentContainer);
this.display.addWordToSidebar(
blacklist,
word,
count,
contentContainer
);
});
this.display.createThresholdDisplay(this.contentEl);
}

View file

@ -29,7 +29,9 @@ describe('ViewManager', () => {
it('should get or create a leaf', () => {
const mockLeaf = {} as WorkspaceLeaf;
(workspace.getLeavesOfType as jest.Mock).mockReturnValueOnce([mockLeaf]);
(workspace.getLeavesOfType as jest.Mock).mockReturnValueOnce([
mockLeaf,
]);
const leaf = viewManager.getOrCreateLeaf(workspace, VIEW_TYPE);
expect(leaf).toBe(mockLeaf);
@ -62,6 +64,8 @@ describe('ViewManager', () => {
viewManager.updateContent();
expect(plugin.frequencyCounter.triggerUpdateContent).toHaveBeenCalledWith(mockEditor);
expect(
plugin.frequencyCounter.triggerUpdateContent
).toHaveBeenCalledWith(mockEditor);
});
});

View file

@ -15,7 +15,10 @@ describe('WordFrequencyCounter tests', () => {
const result = counter.calculateWordFrequencies(content);
expect(result).toEqual([['hello', 2], ['world', 1]]);
expect(result).toEqual([
['hello', 2],
['world', 1],
]);
});
it('should calculate word frequencies with punctuation and mixed case', () => {
@ -23,7 +26,10 @@ describe('WordFrequencyCounter tests', () => {
const result = counter.calculateWordFrequencies(content);
expect(result).toEqual([['hello', 2], ['world', 2]]);
expect(result).toEqual([
['hello', 2],
['world', 2],
]);
});
it('should calculate word frequencies with numbers and special characters', () => {
@ -31,7 +37,12 @@ describe('WordFrequencyCounter tests', () => {
const result = counter.calculateWordFrequencies(content);
expect(result).toEqual([['word1', 2], ['hello', 2], ['word2', 1], ['123', 1]]);
expect(result).toEqual([
['word1', 2],
['hello', 2],
['word2', 1],
['123', 1],
]);
});
it('should calculate word frequencies with periods, colons, and slashes', () => {
@ -69,7 +80,7 @@ describe('WordFrequencyCounter tests', () => {
};
const workspace: Workspace = {} as unknown as Workspace;
const leafMock = {
view: null
view: null,
} as unknown as WorkspaceLeaf;
counterMock.handleActiveLeafChange(leafMock, workspace);
@ -96,7 +107,10 @@ describe('WordFrequencyCounter tests', () => {
counter.handleActiveLeafChange(leafMock, workspace);
expect(containerElMock.addEventListener).toHaveBeenCalledWith('keyup', expect.any(Function));
expect(containerElMock.addEventListener).toHaveBeenCalledWith(
'keyup',
expect.any(Function)
);
});
it('should set lastActiveEditor with a valid MarkdownView', () => {
@ -108,7 +122,9 @@ describe('WordFrequencyCounter tests', () => {
containerEl: containerElMock,
} as unknown as MarkdownView;
const workspace: Workspace = {
getActiveViewOfType: jest.fn().mockReturnValue(markdownViewMock),
getActiveViewOfType: jest
.fn()
.mockReturnValue(markdownViewMock),
getLeavesOfType: jest.fn().mockReturnValue([]),
} as unknown as Workspace;
const leafMock = {
@ -212,7 +228,10 @@ describe('WordFrequencyCounter tests', () => {
it('should not dispatch an event or calculate word frequencies', () => {
const editor = undefined;
const counterMock = jest.spyOn(counter, 'calculateWordFrequencies');
const dispatchEventMock = jest.spyOn(window.document, 'dispatchEvent');
const dispatchEventMock = jest.spyOn(
window.document,
'dispatchEvent'
);
counter.triggerUpdateContent(editor);
@ -225,12 +244,17 @@ describe('WordFrequencyCounter tests', () => {
it('should dispatch an event after calculating word frequencies', () => {
const counterMock = jest.spyOn(counter, 'calculateWordFrequencies');
const dispatchEventMock = jest.spyOn(window.document, 'dispatchEvent');
const expectedValue = 'hello world hello';
const expectedEvent = new CustomEvent(
EVENT_UPDATE,
{ detail: [['hello', 2], ['world', 1]] }
const dispatchEventMock = jest.spyOn(
window.document,
'dispatchEvent'
);
const expectedValue = 'hello world hello';
const expectedEvent = new CustomEvent(EVENT_UPDATE, {
detail: [
['hello', 2],
['world', 1],
],
});
const editor: Editor = {
getValue: jest.fn().mockReturnValue(expectedValue),
} as unknown as Editor;
@ -256,23 +280,33 @@ describe('WordFrequencyCounter tests', () => {
const editor: Editor = {
getValue: jest.fn().mockReturnValue(expectedValue),
} as unknown as Editor;
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {});
counterMock.triggerUpdateContent(editor);
expect(consoleErrorSpy).toHaveBeenCalledWith('error in triggerUpdateContent', expectedError);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'error in triggerUpdateContent',
expectedError
);
consoleErrorSpy.mockRestore();
});
it('should dispatch an event with last active editor when editor is undefined', () => {
const counterMock = jest.spyOn(counter, 'calculateWordFrequencies');
const dispatchEventMock = jest.spyOn(window.document, 'dispatchEvent');
const expectedValue = 'hello world hello';
const expectedEvent = new CustomEvent(
EVENT_UPDATE,
{ detail: [['hello', 2], ['world', 1]] }
const dispatchEventMock = jest.spyOn(
window.document,
'dispatchEvent'
);
const expectedValue = 'hello world hello';
const expectedEvent = new CustomEvent(EVENT_UPDATE, {
detail: [
['hello', 2],
['world', 1],
],
});
const editor: Editor = {
getValue: jest.fn().mockReturnValue(expectedValue),
} as unknown as Editor;

View file

@ -39,7 +39,9 @@ describe('WordFrequencyDisplay', () => {
setText: jest.fn(),
}),
} as unknown as HTMLElement;
blacklist = new Set(mockPlugin.settings.blacklist.split(',').map(word => word.trim()));
blacklist = new Set(
mockPlugin.settings.blacklist.split(',').map((word) => word.trim())
);
display = new WordFrequencyDisplay(mockPlugin, mockView);
});
@ -77,7 +79,8 @@ describe('WordFrequencyDisplay', () => {
addEventListener: jest.fn(),
} as unknown as HTMLButtonElement;
const innerElement = {
createEl: jest.fn()
createEl: jest
.fn()
.mockReturnValueOnce(spanElement)
.mockReturnValueOnce(spanElement)
.mockReturnValueOnce(buttonElement),
@ -93,8 +96,12 @@ describe('WordFrequencyDisplay', () => {
display.addWordToSidebar(blacklist, word, count, contentContainer);
expect(innerElement.createEl).toHaveBeenNthCalledWith(1, 'span', { text: word });
expect(innerElement.createEl).toHaveBeenNthCalledWith(2, 'span', { text: count.toString() });
expect(innerElement.createEl).toHaveBeenNthCalledWith(1, 'span', {
text: word,
});
expect(innerElement.createEl).toHaveBeenNthCalledWith(2, 'span', {
text: count.toString(),
});
expect(innerElement.createEl).toHaveBeenNthCalledWith(3, 'button');
});
@ -104,8 +111,12 @@ describe('WordFrequencyDisplay', () => {
display.saveWordToBlacklist(word);
expect(mockPlugin.settings.blacklist).toBe(`${originalBlacklist},${word}`);
expect(mockPlugin.saveData).toHaveBeenCalledWith(mockPlugin.settings);
expect(mockPlugin.settings.blacklist).toBe(
`${originalBlacklist},${word}`
);
expect(mockPlugin.saveData).toHaveBeenCalledWith(
mockPlugin.settings
);
expect(mockView.updateContent).toHaveBeenCalled();
});
@ -129,11 +140,20 @@ describe('WordFrequencyDisplay', () => {
it('should set text and attribute in the content', () => {
display.createThresholdDisplay(contentEl);
const thresholdDisplay = contentEl.createEl('div', { cls: 'threshold-display' });
const thresholdDisplay = contentEl.createEl('div', {
cls: 'threshold-display',
});
expect(contentEl.createEl).toHaveBeenCalledWith('div', { cls: 'threshold-display' });
expect(thresholdDisplay.setText).toHaveBeenCalledWith(`Current Frequency Threshold is ${mockPlugin.settings.threshold}.`);
expect(thresholdDisplay.setAttr).toHaveBeenCalledWith('title', 'Configure settings for this plugin to update the frequency threshold.');
expect(contentEl.createEl).toHaveBeenCalledWith('div', {
cls: 'threshold-display',
});
expect(thresholdDisplay.setText).toHaveBeenCalledWith(
`Current Frequency Threshold is ${mockPlugin.settings.threshold}.`
);
expect(thresholdDisplay.setAttr).toHaveBeenCalledWith(
'title',
'Configure settings for this plugin to update the frequency threshold.'
);
});
});
});

View file

@ -23,14 +23,18 @@ describe('WordFrequencySettingTab', () => {
plugin = {
settings: {
blacklist: '',
threshold: 0
threshold: 0,
},
saveSettings: jest.fn().mockResolvedValue(undefined),
app: {
workspace: {
getLeavesOfType: jest.fn().mockReturnValue([{ view: { updateContent: jest.fn() } }])
}
}
getLeavesOfType: jest
.fn()
.mockReturnValue([
{ view: { updateContent: jest.fn() } },
]),
},
},
} as unknown as WordFrequencyPlugin;
containerEl = createMockContainerEl();
@ -58,13 +62,17 @@ describe('WordFrequencySettingTab', () => {
await settingTab.updateThreshold(value);
const view = plugin.app.workspace.getLeavesOfType(VIEW_TYPE)[0] as unknown as {
view: WordFrequencyView
const view = plugin.app.workspace.getLeavesOfType(
VIEW_TYPE
)[0] as unknown as {
view: WordFrequencyView;
};
expect(plugin.settings.threshold).toBe(5);
expect(plugin.saveSettings).toHaveBeenCalled();
expect(plugin.app.workspace.getLeavesOfType).toHaveBeenCalledWith(VIEW_TYPE);
expect(plugin.app.workspace.getLeavesOfType).toHaveBeenCalledWith(
VIEW_TYPE
);
expect(view.view.updateContent).toHaveBeenCalled();
});

View file

@ -1,5 +1,10 @@
import { WorkspaceLeaf } from 'obsidian';
import { EVENT_UPDATE, FREQUENCY_ICON, PLUGIN_NAME, VIEW_TYPE } from '../constants';
import {
EVENT_UPDATE,
FREQUENCY_ICON,
PLUGIN_NAME,
VIEW_TYPE,
} from '../constants';
import WordFrequencyPlugin from '../main';
import { WordFrequencyDisplay } from '../WordFrequencyDisplay';
import { WordFrequencyView } from '../WordFrequencyView';
@ -37,7 +42,11 @@ describe('WordFrequencyView', () => {
});
it('should use the provided WordFrequencyDisplay instance if passed', async () => {
const view = new WordFrequencyView(mockLeaf, mockPlugin, mockDisplay);
const view = new WordFrequencyView(
mockLeaf,
mockPlugin,
mockDisplay
);
const viewMock = {
onOpen: view.onOpen,
@ -81,11 +90,17 @@ describe('WordFrequencyView', () => {
describe('onOpen', () => {
it('should add an event listener for EVENT_UPDATE', async () => {
const addEventListenerSpy = jest.spyOn(window.document, 'addEventListener');
const addEventListenerSpy = jest.spyOn(
window.document,
'addEventListener'
);
await view.onOpen();
expect(addEventListenerSpy).toHaveBeenCalledWith(EVENT_UPDATE, expect.any(Function));
expect(addEventListenerSpy).toHaveBeenCalledWith(
EVENT_UPDATE,
expect.any(Function)
);
});
it('should call updateContent on open', async () => {
@ -104,8 +119,13 @@ describe('WordFrequencyView', () => {
await view.onOpen();
const wordCounts = [['apple', 5], ['banana', 3]];
const event = new CustomEvent(EVENT_UPDATE, { detail: { wordCounts } });
const wordCounts = [
['apple', 5],
['banana', 3],
];
const event = new CustomEvent(EVENT_UPDATE, {
detail: { wordCounts },
});
window.document.dispatchEvent(event);
expect(updateContentSpy).toHaveBeenCalled();
@ -117,20 +137,34 @@ describe('WordFrequencyView', () => {
describe('onClose', () => {
it('should remove the event listener for EVENT_UPDATE', async () => {
await view.onOpen();
const removeEventListenerSpy = jest.spyOn(window.document, 'removeEventListener');
const removeEventListenerSpy = jest.spyOn(
window.document,
'removeEventListener'
);
await view.onClose();
expect(removeEventListenerSpy).toHaveBeenCalledWith(EVENT_UPDATE, expect.any(Function));
expect(removeEventListenerSpy).toHaveBeenCalledWith(
EVENT_UPDATE,
expect.any(Function)
);
});
});
describe('updateContent', () => {
it('should add words to sidebar for each word', async () => {
await view.onOpen();
const wordCounts = [['apple', 5], ['banana', 3]];
const event = new CustomEvent(EVENT_UPDATE, { detail: { wordCounts } });
const addWordToSidebarSpy = jest.spyOn(mockDisplay, 'addWordToSidebar');
const wordCounts = [
['apple', 5],
['banana', 3],
];
const event = new CustomEvent(EVENT_UPDATE, {
detail: { wordCounts },
});
const addWordToSidebarSpy = jest.spyOn(
mockDisplay,
'addWordToSidebar'
);
window.document.dispatchEvent(event);

View file

@ -1,5 +1,10 @@
import { App, WorkspaceLeaf, PluginManifest, Workspace } from 'obsidian';
import { DEFAULT_SETTINGS, FREQUENCY_ICON, PLUGIN_NAME, VIEW_TYPE } from '../constants';
import {
DEFAULT_SETTINGS,
FREQUENCY_ICON,
PLUGIN_NAME,
VIEW_TYPE,
} from '../constants';
import WordFrequencyPlugin from '../main';
import { ViewManager } from '../ViewManager';
import { WordFrequencySettingTab } from '../WordFrequencySettingTab';
@ -48,7 +53,11 @@ describe('WordFrequencyPlugin', () => {
updateContent: jest.fn(),
} as unknown as ViewManager;
plugin = new WordFrequencyPlugin(mockApp, mockManifest, mockViewManager);
plugin = new WordFrequencyPlugin(
mockApp,
mockManifest,
mockViewManager
);
plugin['app'] = mockApp;
plugin['loadData'] = jest.fn().mockResolvedValue(DEFAULT_SETTINGS);
plugin['saveData'] = jest.fn().mockResolvedValue(undefined);
@ -68,40 +77,61 @@ describe('WordFrequencyPlugin', () => {
await plugin.onload();
expect(plugin.registerView).toHaveBeenCalledWith(VIEW_TYPE, expect.any(Function));
expect(plugin.registerView).toHaveBeenCalledWith(
VIEW_TYPE,
expect.any(Function)
);
expect(plugin.addRibbonIcon).toHaveBeenCalledWith(
FREQUENCY_ICON,
`Show ${PLUGIN_NAME} Sidebar`,
expect.any(Function)
);
expect(plugin.app.workspace.on).toHaveBeenCalledWith('active-leaf-change', expect.any(Function));
expect(plugin.app.workspace.on).toHaveBeenCalledWith(
'active-leaf-change',
expect.any(Function)
);
expect(plugin.addSettingTab).toHaveBeenCalledWith(settingTab);
});
});
describe('activateView', () => {
it('should set the view state and show the leaf with content', async() => {
it('should set the view state and show the leaf with content', async () => {
const mockLeaf = jest.fn() as unknown as WorkspaceLeaf;
mockViewManager = {
getOrCreateLeaf: jest.fn().mockReturnValue(mockLeaf),
setViewState: jest.fn(),
updateContent: jest.fn(),
} as unknown as ViewManager;
plugin = new WordFrequencyPlugin(mockApp, mockManifest, mockViewManager);
plugin = new WordFrequencyPlugin(
mockApp,
mockManifest,
mockViewManager
);
plugin['app'] = mockApp;
await plugin.activateView();
expect(mockViewManager.getOrCreateLeaf).toHaveBeenCalledWith(plugin.app.workspace, VIEW_TYPE);
expect(mockViewManager.setViewState).toHaveBeenCalledWith(mockLeaf, VIEW_TYPE);
expect(plugin.app.workspace.revealLeaf).toHaveBeenCalledWith(mockLeaf);
expect(mockViewManager.getOrCreateLeaf).toHaveBeenCalledWith(
plugin.app.workspace,
VIEW_TYPE
);
expect(mockViewManager.setViewState).toHaveBeenCalledWith(
mockLeaf,
VIEW_TYPE
);
expect(plugin.app.workspace.revealLeaf).toHaveBeenCalledWith(
mockLeaf
);
expect(mockViewManager.updateContent).toHaveBeenCalled();
});
it('should not set view state or reveal leaf when there is no leaf', async() => {
it('should not set view state or reveal leaf when there is no leaf', async () => {
await plugin.activateView();
expect(mockViewManager.getOrCreateLeaf).toHaveBeenCalledWith(plugin.app.workspace, VIEW_TYPE);
expect(mockViewManager.getOrCreateLeaf).toHaveBeenCalledWith(
plugin.app.workspace,
VIEW_TYPE
);
expect(mockViewManager.setViewState).not.toHaveBeenCalled();
expect(plugin.app.workspace.revealLeaf).not.toHaveBeenCalled();
expect(mockViewManager.updateContent).not.toHaveBeenCalled();

View file

@ -108,4 +108,4 @@ describe('debounce', () => {
expect(mockFunc).toHaveBeenCalledTimes(1);
expect(mockFunc).toHaveReturnedWith('testValue');
});
});
});

View file

@ -1,5 +1,11 @@
import { App, Plugin, PluginManifest, WorkspaceLeaf } from 'obsidian';
import { WordFrequencySettings, DEFAULT_SETTINGS, PLUGIN_NAME, VIEW_TYPE, FREQUENCY_ICON } from './constants';
import {
WordFrequencySettings,
DEFAULT_SETTINGS,
PLUGIN_NAME,
VIEW_TYPE,
FREQUENCY_ICON,
} from './constants';
import { ViewManager } from './ViewManager';
import { WordFrequencyCounter } from './WordFrequencyCounter';
import { WordFrequencySettingTab } from './WordFrequencySettingTab';
@ -11,7 +17,12 @@ export default class WordFrequencyPlugin extends Plugin {
settingTab: WordFrequencySettingTab;
viewManager: ViewManager;
constructor(app: App, manifest: PluginManifest, viewManager?: ViewManager, settingTab?: WordFrequencySettingTab) {
constructor(
app: App,
manifest: PluginManifest,
viewManager?: ViewManager,
settingTab?: WordFrequencySettingTab
) {
super(app, manifest);
this.settingTab = settingTab ?? new WordFrequencySettingTab(this);
this.viewManager = viewManager ?? new ViewManager(this);
@ -26,24 +37,27 @@ export default class WordFrequencyPlugin extends Plugin {
(leaf: WorkspaceLeaf) => new WordFrequencyView(leaf, this)
);
this.addRibbonIcon(FREQUENCY_ICON, `Show ${PLUGIN_NAME} Sidebar`, () => {
this.activateView();
});
this.addRibbonIcon(
FREQUENCY_ICON,
`Show ${PLUGIN_NAME} Sidebar`,
() => {
this.activateView();
}
);
this.registerEvent(
this.app.workspace.on(
'active-leaf-change',
(leaf) => {
this.frequencyCounter.handleActiveLeafChange(leaf, this.app.workspace);
}
)
this.app.workspace.on('active-leaf-change', (leaf) => {
this.frequencyCounter.handleActiveLeafChange(
leaf,
this.app.workspace
);
})
);
this.addSettingTab(this.settingTab);
}
onunload() {
}
onunload() {}
async activateView() {
const { workspace } = this.app;

View file

@ -1,4 +1,7 @@
export function debounce<T extends (...args: Parameters<T>) => void>(func: T, delay: number): (...args: Parameters<T>) => void {
export function debounce<T extends (...args: Parameters<T>) => void>(
func: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>): void => {
clearTimeout(timeoutId);

View file

@ -1,41 +1,41 @@
.setting-item {
display: flex;
align-items: flex-start;
display: flex;
align-items: flex-start;
}
.setting-item-info {
flex-shrink: 0;
flex-shrink: 0;
}
.word-frequency-setting-blacklist {
min-height: 150px;
width: 100%;
min-height: 150px;
width: 100%;
}
.word-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.word-count-container {
display: flex;
justify-content: space-between;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
display: flex;
justify-content: space-between;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.word-count-container span {
flex-shrink: 1;
max-width: 80%;
overflow: hidden;
padding-right: 10px;
flex-shrink: 1;
max-width: 80%;
overflow: hidden;
padding-right: 10px;
}
.button-container {
margin-left: 10px;
margin-top: 0;
margin-left: 10px;
margin-top: 0;
}

View file

@ -4,7 +4,7 @@
"baseUrl": ".",
"esModuleInterop": true,
"jsx": "react",
"lib": ["ESNext", "dom"],
"lib": ["ESNext", "dom"],
"module": "ESNext",
"moduleResolution": "Node",
"outDir": "./dist",
@ -13,10 +13,7 @@
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"types": ["obsidian", "node", "jest"],
"types": ["obsidian", "node", "jest"]
},
"include": [
"src/**/*",
"node_modules/@types/jest/index.d.ts",
]
"include": ["src/**/*", "node_modules/@types/jest/index.d.ts"]
}