mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
tests: happy-dom dom testing, mocks setup helepr, obsidian stub,
timesheetTimer tests
This commit is contained in:
parent
499ba00868
commit
7cd6e7e7d0
12 changed files with 334 additions and 85 deletions
81
package-lock.json
generated
81
package-lock.json
generated
|
|
@ -20,6 +20,7 @@
|
|||
"@types/node": "^20.19.37",
|
||||
"@types/pdfmake": "^0.3.2",
|
||||
"@vitest/coverage-v8": "^4.1.1",
|
||||
"happy-dom": "^20.8.9",
|
||||
"obsidian": "latest",
|
||||
"oxfmt": "^0.42.0",
|
||||
"oxlint": "^1.57.0",
|
||||
|
|
@ -1320,6 +1321,23 @@
|
|||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/whatwg-mimetype": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
||||
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.1.tgz",
|
||||
|
|
@ -1582,6 +1600,19 @@
|
|||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
||||
|
|
@ -1674,6 +1705,24 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/happy-dom": {
|
||||
"version": "20.8.9",
|
||||
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.9.tgz",
|
||||
"integrity": "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": ">=20.0.0",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"entities": "^7.0.1",
|
||||
"whatwg-mimetype": "^3.0.0",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
|
|
@ -2733,6 +2782,16 @@
|
|||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
||||
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
|
|
@ -2750,6 +2809,28 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmldoc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-2.0.3.tgz",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"@types/node": "^20.19.37",
|
||||
"@types/pdfmake": "^0.3.2",
|
||||
"@vitest/coverage-v8": "^4.1.1",
|
||||
"happy-dom": "^20.8.9",
|
||||
"obsidian": "latest",
|
||||
"oxfmt": "^0.42.0",
|
||||
"oxlint": "^1.57.0",
|
||||
|
|
|
|||
|
|
@ -298,3 +298,122 @@ export const MockNotice = vi.fn().mockImplementation(function (
|
|||
) {
|
||||
return {};
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to create the {@link Node.createEl} function added by obsidian
|
||||
*
|
||||
* @param el Container to create elements within
|
||||
* @returns The createDiv function for that container
|
||||
*/
|
||||
function createMockCreateEl(el: Node) {
|
||||
return <K extends keyof HTMLElementTagNameMap>(
|
||||
tag: K,
|
||||
o?: DomElementInfo | string,
|
||||
callback?: (el: HTMLElementTagNameMap[K]) => void
|
||||
): HTMLElementTagNameMap[K] => {
|
||||
const child = document.createElement(tag);
|
||||
|
||||
// Apply options
|
||||
if (o) {
|
||||
if (typeof o === "string") {
|
||||
child.textContent = o;
|
||||
} else {
|
||||
applyDomElementInfo(child, o);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup helpers
|
||||
setObsidianMockElementHelpers(child);
|
||||
|
||||
// Add to the parent
|
||||
el.appendChild(child);
|
||||
|
||||
// Callback from creation
|
||||
if (callback) callback(child);
|
||||
|
||||
return child;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the createEl/createDiv/createSpan helper functions
|
||||
* on the provided node
|
||||
*
|
||||
* @param node The node to add the helper functions to
|
||||
*/
|
||||
export function setObsidianMockElementHelpers(node: Node) {
|
||||
node.createEl = createMockCreateEl(node);
|
||||
node.createDiv = (o?: DomElementInfo | string, callback?: (el: HTMLDivElement) => void) =>
|
||||
node.createEl("div", o, callback);
|
||||
node.createSpan = (o?: DomElementInfo | string, callback?: (el: HTMLDivElement) => void) =>
|
||||
node.createEl("span", o, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the attributes from the DomElementInfo onto
|
||||
* the provided HTML element
|
||||
*
|
||||
* @param el The element to apply to
|
||||
* @param info The dom element info to apply
|
||||
*/
|
||||
function applyDomElementInfo(el: HTMLElement, info: DomElementInfo) {
|
||||
if (info.cls) {
|
||||
if (Array.isArray(info.cls)) {
|
||||
el.classList.add(...info.cls);
|
||||
} else {
|
||||
el.classList.add(info.cls);
|
||||
}
|
||||
}
|
||||
|
||||
if (info.text) {
|
||||
if (info.text instanceof DocumentFragment) {
|
||||
el.appendChild(info.text);
|
||||
} else {
|
||||
el.textContent = info.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.attr) {
|
||||
for (const [key, value] of Object.entries(info.attr)) {
|
||||
if (value === null || value === false) {
|
||||
el.removeAttribute(key);
|
||||
} else {
|
||||
el.setAttribute(key, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info.title) {
|
||||
el.title = info.title;
|
||||
}
|
||||
|
||||
if (info.parent) {
|
||||
if (info.prepend && info.parent.firstChild) {
|
||||
info.parent.insertBefore(el, info.parent.firstChild);
|
||||
} else {
|
||||
info.parent.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
if (el instanceof HTMLInputElement) {
|
||||
if (info.value !== undefined) {
|
||||
el.value = info.value;
|
||||
}
|
||||
if (info.placeholder !== undefined) {
|
||||
el.placeholder = info.placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.href && el instanceof HTMLAnchorElement) {
|
||||
el.href = info.href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for creating a mock container element extended with
|
||||
*/
|
||||
export function createMockContainer() {
|
||||
const container = document.createElement("div");
|
||||
setObsidianMockElementHelpers(container);
|
||||
return container;
|
||||
}
|
||||
|
|
|
|||
7
src/__mocks__/obsidianStub.ts
Normal file
7
src/__mocks__/obsidianStub.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Stub file for testing module resolution, this file doesn't contain anything just
|
||||
* vitest needs a module that it can resolve for browser testing mode otherwise
|
||||
* it will error from being unable to resolve the "obsidian" module
|
||||
*/
|
||||
|
||||
export {};
|
||||
21
src/__mocks__/setupObsidianMocks.ts
Normal file
21
src/__mocks__/setupObsidianMocks.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { vi } from "vitest";
|
||||
|
||||
import {
|
||||
MockComponent,
|
||||
MockNotice,
|
||||
MockTAbstractFile,
|
||||
MockTFile,
|
||||
MockTFolder,
|
||||
MockVault,
|
||||
} from "@/__mocks__/obsidian";
|
||||
|
||||
vi.mock("obsidian", () => {
|
||||
return {
|
||||
TFile: MockTFile,
|
||||
TFolder: MockTFolder,
|
||||
TAbstractFile: MockTAbstractFile,
|
||||
Vault: MockVault,
|
||||
Component: MockComponent,
|
||||
Notice: MockNotice,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,30 +1,12 @@
|
|||
import type { App } from "obsidian";
|
||||
|
||||
import { describe, vi, it, expect } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import {
|
||||
MockComponent,
|
||||
MockNotice,
|
||||
MockTAbstractFile,
|
||||
MockTFile,
|
||||
MockTFolder,
|
||||
MockVault,
|
||||
} from "@/__mocks__/obsidian";
|
||||
import { MockNotice, MockVault } from "@/__mocks__/obsidian";
|
||||
import { createCodeBlock } from "@/utils/codeblock";
|
||||
|
||||
import stopAllTimekeeps from "./stopAllTimekeeps";
|
||||
|
||||
vi.mock("obsidian", () => {
|
||||
return {
|
||||
TFile: MockTFile,
|
||||
TFolder: MockTFolder,
|
||||
TAbstractFile: MockTAbstractFile,
|
||||
Vault: MockVault,
|
||||
Component: MockComponent,
|
||||
Notice: MockNotice,
|
||||
};
|
||||
});
|
||||
|
||||
describe("stopFileTimekeeps", () => {
|
||||
it("when nothing is stopped should get a notice", async () => {
|
||||
const vault = new MockVault();
|
||||
|
|
|
|||
|
|
@ -2,29 +2,11 @@ import type { App } from "obsidian";
|
|||
|
||||
import { describe, vi, it, expect } from "vitest";
|
||||
|
||||
import {
|
||||
MockComponent,
|
||||
MockNotice,
|
||||
MockTAbstractFile,
|
||||
MockTFile,
|
||||
MockTFolder,
|
||||
MockVault,
|
||||
} from "@/__mocks__/obsidian";
|
||||
import { MockNotice, MockVault } from "@/__mocks__/obsidian";
|
||||
import { createCodeBlock } from "@/utils/codeblock";
|
||||
|
||||
import stopFileTimekeeps from "./stopFileTimekeeps";
|
||||
|
||||
vi.mock("obsidian", () => {
|
||||
return {
|
||||
TFile: MockTFile,
|
||||
TFolder: MockTFolder,
|
||||
TAbstractFile: MockTAbstractFile,
|
||||
Vault: MockVault,
|
||||
Component: MockComponent,
|
||||
Notice: MockNotice,
|
||||
};
|
||||
});
|
||||
|
||||
describe("stopFileTimekeeps", () => {
|
||||
it("when no active file is detected should get a notice", async () => {
|
||||
const app = {
|
||||
|
|
|
|||
95
src/components/timesheetTimer.test.ts
Normal file
95
src/components/timesheetTimer.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, it, beforeEach, expect } from "vitest";
|
||||
|
||||
import { createMockContainer } from "@/__mocks__/obsidian";
|
||||
|
||||
import { TimesheetTimer } from "./timesheetTimer";
|
||||
|
||||
describe("TimesheetTimer", () => {
|
||||
let container: ReturnType<typeof createMockContainer>;
|
||||
let timer: TimesheetTimer;
|
||||
|
||||
beforeEach(() => {
|
||||
container = createMockContainer();
|
||||
timer = new TimesheetTimer(container, "Test Label");
|
||||
});
|
||||
|
||||
it("should create timer elements on load", () => {
|
||||
timer.onload();
|
||||
|
||||
const timerEl = container.children[0] as HTMLElement;
|
||||
expect(timerEl).toBeDefined();
|
||||
expect(timerEl.className).toBe("timekeep-timer");
|
||||
|
||||
const primary = timerEl.children[0] as HTMLElement;
|
||||
const secondary = timerEl.children[1] as HTMLElement;
|
||||
const label = timerEl.children[2] as HTMLElement;
|
||||
|
||||
expect(primary.className).toBe("timekeep-timer-value");
|
||||
expect(secondary.className).toBe("timekeep-timer-value-small");
|
||||
expect(label.textContent).toBe("Test Label");
|
||||
|
||||
expect(primary.textContent).toBe("");
|
||||
expect(secondary.textContent).toBe(" ");
|
||||
expect(secondary.hidden).toBe(false);
|
||||
});
|
||||
|
||||
it("should remove timer element on unload", () => {
|
||||
timer.onload();
|
||||
const timerEl = container.children[0] as HTMLElement;
|
||||
expect(timerEl).toBeDefined();
|
||||
|
||||
timer.onunload();
|
||||
expect(timerEl.parentElement).toBeNull();
|
||||
});
|
||||
|
||||
it("should hide or show the timer element", () => {
|
||||
timer.onload();
|
||||
const timerEl = container.children[0] as HTMLElement;
|
||||
|
||||
timer.setHidden(true);
|
||||
expect(timerEl.hidden).toBe(true);
|
||||
|
||||
timer.setHidden(false);
|
||||
expect(timerEl.hidden).toBe(false);
|
||||
});
|
||||
|
||||
it("should set primary and secondary values correctly", () => {
|
||||
timer.onload();
|
||||
const timerEl = container.children[0] as HTMLElement;
|
||||
const primary = timerEl.children[0] as HTMLDivElement;
|
||||
const secondary = timerEl.children[1] as HTMLDivElement;
|
||||
|
||||
timer.setValues("1h 55m 30s", "1.9h");
|
||||
expect(primary.textContent).toBe("1h 55m 30s");
|
||||
expect(secondary.textContent).toBe("1.9h");
|
||||
expect(secondary.hidden).toBe(false);
|
||||
|
||||
timer.setValues("2h 0m 5s", "");
|
||||
expect(primary.textContent).toBe("2h 0m 5s");
|
||||
expect(secondary.textContent).toBe("");
|
||||
expect(secondary.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it("secondary value should be hidden if it is empty", () => {
|
||||
timer.onload();
|
||||
|
||||
const timerEl = container.children[0] as HTMLElement;
|
||||
const secondary = timerEl.children[1] as HTMLDivElement;
|
||||
|
||||
timer.setValues("1h 55m 30s", "1.9h");
|
||||
expect(secondary.hidden).toBe(false);
|
||||
|
||||
timer.setValues("2h 0m 5s", "");
|
||||
expect(secondary.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it("should not throw if setValues called before onload", () => {
|
||||
expect(() => timer.setValues("1", "2")).not.toThrow();
|
||||
});
|
||||
|
||||
it("should not throw if setHidden called before onload", () => {
|
||||
expect(() => timer.setHidden(true)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
@ -28,20 +28,11 @@ export class TimesheetTimer extends Component {
|
|||
onload(): void {
|
||||
super.onload();
|
||||
|
||||
const timerEl = this.#containerEl.createDiv({
|
||||
cls: "timekeep-timer",
|
||||
});
|
||||
|
||||
const timerEl = this.#containerEl.createDiv({ cls: "timekeep-timer" });
|
||||
this.#timerEl = timerEl;
|
||||
|
||||
const primaryValueEl = timerEl.createDiv({
|
||||
cls: "timekeep-timer-value",
|
||||
});
|
||||
|
||||
const secondaryValueEl = timerEl.createDiv({
|
||||
cls: "timekeep-timer-value-small",
|
||||
});
|
||||
|
||||
const primaryValueEl = timerEl.createDiv({ cls: "timekeep-timer-value" });
|
||||
const secondaryValueEl = timerEl.createDiv({ cls: "timekeep-timer-value-small" });
|
||||
timerEl.createSpan({ text: this.#label });
|
||||
|
||||
this.#primaryValueEl = primaryValueEl;
|
||||
|
|
|
|||
|
|
@ -2,13 +2,7 @@ import moment from "moment";
|
|||
import { v4 } from "uuid";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
MockComponent,
|
||||
MockTAbstractFile,
|
||||
MockTFile,
|
||||
MockTFolder,
|
||||
MockVault,
|
||||
} from "@/__mocks__/obsidian";
|
||||
import { MockVault } from "@/__mocks__/obsidian";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { createStore } from "@/store";
|
||||
import { Timekeep } from "@/timekeep/schema";
|
||||
|
|
@ -17,16 +11,6 @@ import { createCodeBlock } from "@/utils/codeblock";
|
|||
import { TimekeepAutocomplete } from "./autocomplete";
|
||||
import { TimekeepRegistry } from "./registry";
|
||||
|
||||
vi.mock("obsidian", () => {
|
||||
return {
|
||||
TFile: MockTFile,
|
||||
TFolder: MockTFolder,
|
||||
TAbstractFile: MockTAbstractFile,
|
||||
Vault: MockVault,
|
||||
Component: MockComponent,
|
||||
};
|
||||
});
|
||||
|
||||
describe("TimekeepAutocomplete", () => {
|
||||
describe("autocomplete enabled", () => {
|
||||
it("empty vault should have no names", async () => {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,7 @@ import moment from "moment";
|
|||
import { v4 } from "uuid";
|
||||
import { describe, vi, it, expect } from "vitest";
|
||||
|
||||
import {
|
||||
MockComponent,
|
||||
MockTAbstractFile,
|
||||
MockTFile,
|
||||
MockTFolder,
|
||||
MockVault,
|
||||
} from "@/__mocks__/obsidian";
|
||||
import { MockVault } from "@/__mocks__/obsidian";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { createStore } from "@/store";
|
||||
import { stripTimekeepRuntimeData, Timekeep } from "@/timekeep/schema";
|
||||
|
|
@ -16,16 +10,6 @@ import { createCodeBlock } from "@/utils/codeblock";
|
|||
|
||||
import { TimekeepEntryItemType, TimekeepRegistry, TimekeepRegistryEntryMarkdown } from "./registry";
|
||||
|
||||
vi.mock("obsidian", () => {
|
||||
return {
|
||||
TFile: MockTFile,
|
||||
TFolder: MockTFolder,
|
||||
TAbstractFile: MockTAbstractFile,
|
||||
Vault: MockVault,
|
||||
Component: MockComponent,
|
||||
};
|
||||
});
|
||||
|
||||
describe("TimekeepRegistry", () => {
|
||||
describe("getFileRegistryEntry", () => {
|
||||
it("returns null for markdown without timekeeps", async () => {
|
||||
|
|
|
|||
|
|
@ -51,10 +51,12 @@ export default defineConfig((env) => ({
|
|||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
obsidian: path.resolve(__dirname, "src", "__mocks__", "obsidianStub"),
|
||||
},
|
||||
},
|
||||
|
||||
test: {
|
||||
setupFiles: path.resolve(__dirname, "src", "__mocks__", "setupObsidianMocks.ts"),
|
||||
coverage: {
|
||||
// Exclude mocks and fixtures from coverage
|
||||
exclude: ["**/__mocks__/**", "**/__fixtures__/**"],
|
||||
|
|
|
|||
Loading…
Reference in a new issue