mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
feat: mock component handle loading and unload edge cases, split start
block into form and running container, tests for start form
This commit is contained in:
parent
a6cade73f6
commit
dbe4ac4a9e
10 changed files with 541 additions and 145 deletions
|
|
@ -251,36 +251,44 @@ export class MockVault {
|
|||
export class MockComponent {
|
||||
private children: Component[] = [];
|
||||
private loaded = false;
|
||||
private events: EventRef[] = [];
|
||||
private callbacks: VoidFunction[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
load() {
|
||||
if (this.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
this.onload();
|
||||
this.children.map((c) => c.load());
|
||||
this.children.forEach((c) => c.load());
|
||||
}
|
||||
|
||||
unload() {
|
||||
this.children.forEach((c) => c.unload());
|
||||
this.onunload();
|
||||
|
||||
for (const eventRef of this.events) {
|
||||
const ref = eventRef as MockEventRef;
|
||||
ref.callback();
|
||||
if (!this.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = false;
|
||||
|
||||
this.children.forEach((c) => c.unload());
|
||||
|
||||
for (const callback of this.callbacks) {
|
||||
callback();
|
||||
}
|
||||
|
||||
this.loaded = false;
|
||||
this.onunload();
|
||||
}
|
||||
|
||||
onload() {}
|
||||
onunload() {}
|
||||
|
||||
registerEvent(eventRef: EventRef) {
|
||||
this.events.push(eventRef);
|
||||
this.callbacks.push(() => {
|
||||
const ref = eventRef as MockEventRef;
|
||||
ref.callback();
|
||||
});
|
||||
}
|
||||
|
||||
register(callback: VoidFunction) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import { Timekeep } from "@/timekeep/schema";
|
|||
import { DomComponent } from "./domComponent";
|
||||
import { TimesheetCounters } from "./timesheetCounters";
|
||||
import { TimesheetExportActions } from "./timesheetExportActions";
|
||||
import { TimesheetStart } from "./timesheetStart";
|
||||
import { TimesheetStartContainer } from "./timesheetStartContainer";
|
||||
import { TimesheetStartForm } from "./timesheetStartForm";
|
||||
import { TimesheetTable } from "./timesheetTable";
|
||||
|
||||
/**
|
||||
|
|
@ -53,15 +54,19 @@ export class TimesheetApp extends DomComponent {
|
|||
|
||||
this.wrapperEl = wrapperEl;
|
||||
|
||||
console.log(this);
|
||||
|
||||
const counters = new TimesheetCounters(wrapperEl, this.settings, this.timekeep);
|
||||
|
||||
const start = new TimesheetStart(
|
||||
const startForm = new TimesheetStartForm(
|
||||
wrapperEl,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
this.autocomplete
|
||||
);
|
||||
|
||||
const startContainer = new TimesheetStartContainer(wrapperEl, this.timekeep, this.settings);
|
||||
|
||||
const table = new TimesheetTable(wrapperEl, this.app, this.timekeep, this.settings);
|
||||
|
||||
const exportActions = new TimesheetExportActions(
|
||||
|
|
@ -73,7 +78,8 @@ export class TimesheetApp extends DomComponent {
|
|||
);
|
||||
|
||||
this.addChild(counters);
|
||||
this.addChild(start);
|
||||
this.addChild(startForm);
|
||||
this.addChild(startContainer);
|
||||
this.addChild(table);
|
||||
this.addChild(exportActions);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { beforeEach, it, describe } from "vitest";
|
||||
|
||||
import type { TimekeepSettings } from "@/settings";
|
||||
import type { Store } from "@/store";
|
||||
import type { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
import { createMockContainer, MockVault } from "@/__mocks__/obsidian";
|
||||
import { TimekeepAutocomplete } from "@/service/autocomplete";
|
||||
import { TimekeepRegistry } from "@/service/registry";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { createStore } from "@/store";
|
||||
|
||||
import { TimesheetStart } from "./timesheetStart";
|
||||
|
||||
describe("TimesheetStart", () => {
|
||||
let containerEl: HTMLElement;
|
||||
let vault: MockVault;
|
||||
|
||||
let timekeep: Store<Timekeep>;
|
||||
let settings: Store<TimekeepSettings>;
|
||||
let registry: TimekeepRegistry;
|
||||
let autocomplete: TimekeepAutocomplete;
|
||||
|
||||
beforeEach(() => {
|
||||
vault = new MockVault();
|
||||
containerEl = createMockContainer();
|
||||
timekeep = createStore({ entries: [] });
|
||||
settings = createStore(defaultSettings);
|
||||
|
||||
registry = new TimekeepRegistry(vault.asVault(), settings);
|
||||
autocomplete = new TimekeepAutocomplete(registry, settings);
|
||||
});
|
||||
|
||||
it("should load without error", () => {
|
||||
const component = new TimesheetStart(containerEl, timekeep, settings, autocomplete);
|
||||
component.load();
|
||||
});
|
||||
});
|
||||
142
src/components/timesheetStartContainer.test.ts
Normal file
142
src/components/timesheetStartContainer.test.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import moment from "moment";
|
||||
import { v4 } from "uuid";
|
||||
import { beforeEach, it, describe, expect, afterEach } from "vitest";
|
||||
|
||||
import type { TimekeepSettings } from "@/settings";
|
||||
import type { Store } from "@/store";
|
||||
import type { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
import { createMockContainer } from "@/__mocks__/obsidian";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { createStore } from "@/store";
|
||||
|
||||
import { TimesheetStartContainer } from "./timesheetStartContainer";
|
||||
import { TimesheetStartEditing } from "./timesheetStartEditing";
|
||||
import { TimesheetStartRunning } from "./timesheetStartRunning";
|
||||
|
||||
describe("TimesheetStartContainer", () => {
|
||||
let containerEl: HTMLElement;
|
||||
let timekeep: Store<Timekeep>;
|
||||
let settings: Store<TimekeepSettings>;
|
||||
let component: TimesheetStartContainer;
|
||||
|
||||
beforeEach(() => {
|
||||
containerEl = createMockContainer();
|
||||
timekeep = createStore({ entries: [] });
|
||||
settings = createStore(defaultSettings);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (component) component.unload();
|
||||
});
|
||||
|
||||
it("should load without error", () => {
|
||||
component = new TimesheetStartContainer(containerEl, timekeep, settings);
|
||||
component.load();
|
||||
});
|
||||
|
||||
it("should be undefined content without a running entry", () => {
|
||||
component = new TimesheetStartContainer(containerEl, timekeep, settings);
|
||||
component.load();
|
||||
expect(component.getContent()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should be TimesheetStartRunning when theres a running entry", () => {
|
||||
const start = moment();
|
||||
timekeep.setState({
|
||||
entries: [
|
||||
{
|
||||
id: v4(),
|
||||
name: "Test",
|
||||
startTime: moment(start),
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
component = new TimesheetStartContainer(containerEl, timekeep, settings);
|
||||
component.load();
|
||||
|
||||
expect(component.getContent()).toBeInstanceOf(TimesheetStartRunning);
|
||||
});
|
||||
|
||||
it("should be TimesheetStartEditing when theres a running entry and edit is pressed", () => {
|
||||
const start = moment();
|
||||
timekeep.setState({
|
||||
entries: [
|
||||
{
|
||||
id: v4(),
|
||||
name: "Test",
|
||||
startTime: moment(start),
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
component = new TimesheetStartContainer(containerEl, timekeep, settings);
|
||||
component.load();
|
||||
|
||||
const content = component.getContent() as TimesheetStartRunning;
|
||||
content.onStartEditing();
|
||||
|
||||
expect(component.getContent()).toBeInstanceOf(TimesheetStartEditing);
|
||||
});
|
||||
|
||||
it("timekeep updates when editing should update the running entry", () => {
|
||||
const start = moment();
|
||||
timekeep.setState({
|
||||
entries: [
|
||||
{
|
||||
id: v4(),
|
||||
name: "Test",
|
||||
startTime: moment(start),
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
component = new TimesheetStartContainer(containerEl, timekeep, settings);
|
||||
component.load();
|
||||
|
||||
const content = component.getContent() as TimesheetStartRunning;
|
||||
content.onStartEditing();
|
||||
|
||||
expect(component.getContent()).toBeInstanceOf(TimesheetStartEditing);
|
||||
|
||||
timekeep.setState(timekeep.getState());
|
||||
|
||||
expect(component.getContent()).toBeInstanceOf(TimesheetStartEditing);
|
||||
});
|
||||
|
||||
it("after a timekeep update when editing if theres no more current entry should return to undefined state", () => {
|
||||
const start = moment();
|
||||
timekeep.setState({
|
||||
entries: [
|
||||
{
|
||||
id: v4(),
|
||||
name: "Test",
|
||||
startTime: moment(start),
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
component = new TimesheetStartContainer(containerEl, timekeep, settings);
|
||||
component.load();
|
||||
|
||||
const content = component.getContent() as TimesheetStartRunning;
|
||||
content.onStartEditing();
|
||||
|
||||
expect(component.getContent()).toBeInstanceOf(TimesheetStartEditing);
|
||||
|
||||
timekeep.setState({ entries: [] });
|
||||
|
||||
expect(component.getContent()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
97
src/components/timesheetStartContainer.ts
Normal file
97
src/components/timesheetStartContainer.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import type { TimekeepSettings } from "@/settings";
|
||||
import type { Store } from "@/store";
|
||||
import type { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
import { getRunningEntry } from "@/timekeep/queries";
|
||||
import { assert } from "@/utils/assert";
|
||||
|
||||
import { ContentComponent } from "./contentComponent";
|
||||
import { TimesheetStartEditing } from "./timesheetStartEditing";
|
||||
import { TimesheetStartRunning } from "./timesheetStartRunning";
|
||||
|
||||
export class TimesheetStartContainer extends ContentComponent<
|
||||
TimesheetStartRunning | TimesheetStartEditing
|
||||
> {
|
||||
/** Access to the timekeep */
|
||||
timekeep: Store<Timekeep>;
|
||||
/** Access to the timekeep settings */
|
||||
settings: Store<TimekeepSettings>;
|
||||
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
timekeep: Store<Timekeep>,
|
||||
settings: Store<TimekeepSettings>
|
||||
) {
|
||||
super(containerEl);
|
||||
|
||||
this.timekeep = timekeep;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
|
||||
const onUpdate = this.onUpdate.bind(this);
|
||||
this.register(this.timekeep.subscribe(onUpdate));
|
||||
onUpdate();
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
if (this.getContent() instanceof TimesheetStartEditing) {
|
||||
this.setEditingView();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setCurrentView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to the editing view
|
||||
*/
|
||||
setEditingView() {
|
||||
const contentEl = this.containerEl;
|
||||
assert(contentEl, "Content element should be defined");
|
||||
|
||||
const timekeep = this.timekeep.getState();
|
||||
const currentEntry = getRunningEntry(timekeep.entries);
|
||||
if (!currentEntry) {
|
||||
this.setContent(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setContent(
|
||||
new TimesheetStartEditing(
|
||||
contentEl,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
currentEntry.name,
|
||||
this.setCurrentView.bind(this)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to the default creation view
|
||||
*/
|
||||
setCurrentView() {
|
||||
const contentEl = this.containerEl;
|
||||
assert(contentEl, "Content element should be defined");
|
||||
|
||||
const timekeep = this.timekeep.getState();
|
||||
const currentEntry = getRunningEntry(timekeep.entries);
|
||||
if (!currentEntry) {
|
||||
this.setContent(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setContent(
|
||||
new TimesheetStartRunning(
|
||||
contentEl,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
currentEntry,
|
||||
this.setEditingView.bind(this)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { beforeEach, it, describe, vi } from "vitest";
|
||||
import moment from "moment";
|
||||
import { v4 } from "uuid";
|
||||
import { beforeEach, it, describe, vi, expect } from "vitest";
|
||||
|
||||
import type { TimekeepSettings } from "@/settings";
|
||||
import type { Store } from "@/store";
|
||||
|
|
@ -35,4 +37,69 @@ describe("TimesheetStartEditing", () => {
|
|||
);
|
||||
component.load();
|
||||
});
|
||||
|
||||
it("should save nothing if the running entry doesn't exist", () => {
|
||||
const component = new TimesheetStartEditing(
|
||||
containerEl,
|
||||
timekeep,
|
||||
settings,
|
||||
"Test Entry",
|
||||
onFinishedEditing
|
||||
);
|
||||
|
||||
const onSave = vi.spyOn(component, "onSave");
|
||||
|
||||
component.load();
|
||||
|
||||
const formEl = component.wrapperEl!;
|
||||
(formEl as HTMLFormElement).dispatchEvent(
|
||||
new SubmitEvent("submit", { bubbles: true, cancelable: false })
|
||||
);
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should be able to save the running entry", () => {
|
||||
const start = moment();
|
||||
const end = moment().add(1, "hour");
|
||||
vi.setSystemTime(end.toDate());
|
||||
|
||||
const entry = {
|
||||
id: v4(),
|
||||
name: "Test",
|
||||
startTime: moment(start),
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
};
|
||||
|
||||
timekeep.setState({
|
||||
entries: [entry],
|
||||
});
|
||||
|
||||
const component = new TimesheetStartEditing(
|
||||
containerEl,
|
||||
timekeep,
|
||||
settings,
|
||||
"Test Entry",
|
||||
onFinishedEditing
|
||||
);
|
||||
|
||||
const onSave = vi.spyOn(component, "onSave");
|
||||
|
||||
component.load();
|
||||
|
||||
const formEl = component.wrapperEl!;
|
||||
|
||||
const nameInputEl = formEl.querySelector(".timekeep-name");
|
||||
expect(nameInputEl).not.toBeNull();
|
||||
(nameInputEl as HTMLInputElement).value = "New Name";
|
||||
|
||||
(formEl as HTMLFormElement).dispatchEvent(
|
||||
new SubmitEvent("submit", { bubbles: true, cancelable: false })
|
||||
);
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
|
||||
expect(timekeep.getState()).toEqual({
|
||||
entries: [{ ...entry, name: "New Name" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
83
src/components/timesheetStartForm.test.ts
Normal file
83
src/components/timesheetStartForm.test.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import moment from "moment";
|
||||
import { beforeEach, it, describe, afterEach, vi, expect } from "vitest";
|
||||
|
||||
import type { TimekeepSettings } from "@/settings";
|
||||
import type { Store } from "@/store";
|
||||
import type { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
import { createMockContainer, MockVault } from "@/__mocks__/obsidian";
|
||||
import { TimekeepAutocomplete } from "@/service/autocomplete";
|
||||
import { TimekeepRegistry } from "@/service/registry";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { createStore } from "@/store";
|
||||
|
||||
import { TimesheetStartForm } from "./timesheetStartForm";
|
||||
|
||||
vi.mock(import("uuid"), async (importOriginal) => {
|
||||
return {
|
||||
...(await importOriginal()),
|
||||
v4: vi.fn(() => "mocked-uuid"),
|
||||
};
|
||||
});
|
||||
|
||||
describe("TimesheetStart", () => {
|
||||
let containerEl: HTMLElement;
|
||||
let vault: MockVault;
|
||||
|
||||
let timekeep: Store<Timekeep>;
|
||||
let settings: Store<TimekeepSettings>;
|
||||
let registry: TimekeepRegistry;
|
||||
let autocomplete: TimekeepAutocomplete;
|
||||
let component: TimesheetStartForm;
|
||||
|
||||
beforeEach(() => {
|
||||
vault = new MockVault();
|
||||
containerEl = createMockContainer();
|
||||
timekeep = createStore({ entries: [] });
|
||||
settings = createStore(defaultSettings);
|
||||
|
||||
registry = new TimekeepRegistry(vault.asVault(), settings);
|
||||
autocomplete = new TimekeepAutocomplete(registry, settings);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (component) component.unload();
|
||||
});
|
||||
|
||||
it("should load without error", () => {
|
||||
component = new TimesheetStartForm(containerEl, timekeep, settings, autocomplete);
|
||||
component.load();
|
||||
});
|
||||
|
||||
it("clicking start should start a new entry with the name", () => {
|
||||
const start = moment();
|
||||
|
||||
vi.setSystemTime(start.toDate());
|
||||
|
||||
component = new TimesheetStartForm(containerEl, timekeep, settings, autocomplete);
|
||||
|
||||
const onStart = vi.spyOn(component, "onStart");
|
||||
component.load();
|
||||
|
||||
const formEl = component.wrapperEl as HTMLFormElement;
|
||||
const nameInputEl = formEl.querySelector("#timekeepBlockName");
|
||||
(nameInputEl as HTMLInputElement).value = "Test";
|
||||
|
||||
formEl.dispatchEvent(new SubmitEvent("submit", { bubbles: true, cancelable: true }));
|
||||
expect(onStart).toHaveBeenCalled();
|
||||
|
||||
expect(timekeep.getState()).toEqual({
|
||||
entries: [
|
||||
{
|
||||
id: "mocked-uuid",
|
||||
name: "Test",
|
||||
startTime: moment(start),
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -9,18 +9,14 @@ import { getRunningEntry } from "@/timekeep/queries";
|
|||
import { startNewEntry } from "@/timekeep/start";
|
||||
import { assert } from "@/utils/assert";
|
||||
|
||||
import { ContentComponent } from "./contentComponent";
|
||||
import { DomComponent } from "./domComponent";
|
||||
import { createObsidianIcon } from "./obsidianIcon";
|
||||
import { TimesheetNameInput } from "./timesheetNameInput";
|
||||
import { TimesheetStartEditing } from "./timesheetStartEditing";
|
||||
import { TimesheetStartRunning } from "./timesheetStartRunning";
|
||||
|
||||
/**
|
||||
* The start section above the timesheet table
|
||||
*/
|
||||
export class TimesheetStart extends ContentComponent<
|
||||
TimesheetStartRunning | TimesheetStartEditing
|
||||
> {
|
||||
export class TimesheetStartForm extends DomComponent {
|
||||
/** Access to the timekeep */
|
||||
timekeep: Store<Timekeep>;
|
||||
/** Access to the timekeep settings */
|
||||
|
|
@ -28,9 +24,6 @@ export class TimesheetStart extends ContentComponent<
|
|||
/** Access to autocomplete */
|
||||
autocomplete: TimekeepAutocomplete;
|
||||
|
||||
/** Content container element */
|
||||
#contentEl: HTMLElement | undefined;
|
||||
|
||||
/** Name input for starting entries */
|
||||
#nameInput: TimesheetNameInput | undefined;
|
||||
|
||||
|
|
@ -54,16 +47,10 @@ export class TimesheetStart extends ContentComponent<
|
|||
onload(): void {
|
||||
super.onload();
|
||||
|
||||
const wrapperEl = this.containerEl.createDiv();
|
||||
this.wrapperEl = wrapperEl;
|
||||
|
||||
const contentEl = wrapperEl.createDiv();
|
||||
this.#contentEl = contentEl;
|
||||
|
||||
this.setCurrentView();
|
||||
|
||||
const formEl = wrapperEl.createEl("form", { cls: "timekeep-start-area" });
|
||||
const formEl = this.containerEl.createEl("form", { cls: "timekeep-start-area" });
|
||||
formEl.setAttribute("data-area", "start");
|
||||
this.wrapperEl = formEl;
|
||||
|
||||
this.registerDomEvent(formEl, "submit", this.onStart.bind(this));
|
||||
|
||||
const nameWrapperEl = formEl.createDiv({ cls: "timekeep-name-wrapper" });
|
||||
|
|
@ -92,85 +79,23 @@ export class TimesheetStart extends ContentComponent<
|
|||
this.#startButtonEl = startButton;
|
||||
|
||||
const onUpdate = this.onUpdate.bind(this);
|
||||
const unsubscribeTimekeep = this.timekeep.subscribe(onUpdate);
|
||||
this.register(this.timekeep.subscribe(onUpdate));
|
||||
onUpdate();
|
||||
|
||||
this.register(unsubscribeTimekeep);
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
this.updateRunning();
|
||||
const timekeep = this.timekeep.getState();
|
||||
const currentEntry = getRunningEntry(timekeep.entries);
|
||||
|
||||
if (this.getContent() instanceof TimesheetStartEditing) {
|
||||
this.setEditingView();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setCurrentView();
|
||||
}
|
||||
|
||||
updateRunning() {
|
||||
const blockPauseWarningEl = this.#blockPauseWarningEl;
|
||||
const startButtonEl = this.#startButtonEl;
|
||||
assert(blockPauseWarningEl && startButtonEl, "Elements should be defined");
|
||||
|
||||
const timekeep = this.timekeep.getState();
|
||||
const currentEntry = getRunningEntry(timekeep.entries);
|
||||
const isTimekeepRunning = currentEntry !== null;
|
||||
|
||||
blockPauseWarningEl.hidden = currentEntry === null || currentEntry.startTime === null;
|
||||
startButtonEl.title = isTimekeepRunning ? "Stop and start" : "Start";
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to the editing view
|
||||
*/
|
||||
setEditingView() {
|
||||
const contentEl = this.#contentEl;
|
||||
assert(contentEl, "Content element should be defined");
|
||||
|
||||
const timekeep = this.timekeep.getState();
|
||||
const currentEntry = getRunningEntry(timekeep.entries);
|
||||
if (!currentEntry) {
|
||||
return this.setContent(undefined);
|
||||
}
|
||||
|
||||
this.setContent(
|
||||
new TimesheetStartEditing(
|
||||
contentEl,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
currentEntry.name,
|
||||
this.setCurrentView.bind(this)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to the default creation view
|
||||
*/
|
||||
setCurrentView() {
|
||||
const contentEl = this.#contentEl;
|
||||
assert(contentEl, "Content element should be defined");
|
||||
|
||||
const timekeep = this.timekeep.getState();
|
||||
const currentEntry = getRunningEntry(timekeep.entries);
|
||||
|
||||
if (currentEntry === null || currentEntry.startTime === null) {
|
||||
return this.setContent(undefined);
|
||||
}
|
||||
|
||||
this.setContent(
|
||||
new TimesheetStartRunning(
|
||||
contentEl,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
currentEntry,
|
||||
this.setEditingView.bind(this)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
onStart(event: Event) {
|
||||
// Prevent form submission from reloading Obsidian
|
||||
event.preventDefault();
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import moment from "moment";
|
||||
import { v4 } from "uuid";
|
||||
import { describe, it, vi, beforeEach } from "vitest";
|
||||
import { describe, it, vi, beforeEach, afterEach, expect } from "vitest";
|
||||
|
||||
import type { TimeEntry, Timekeep } from "@/timekeep/schema";
|
||||
|
||||
|
|
@ -17,6 +17,7 @@ describe("TimesheetStartRunning", () => {
|
|||
let timekeep: Store<Timekeep>;
|
||||
let settings: Store<TimekeepSettings>;
|
||||
let onStartEditing: VoidFunction;
|
||||
let component: TimesheetStartRunning;
|
||||
|
||||
const start = moment();
|
||||
const entry: TimeEntry = {
|
||||
|
|
@ -42,8 +43,12 @@ describe("TimesheetStartRunning", () => {
|
|||
onStartEditing = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (component) component.unload();
|
||||
});
|
||||
|
||||
it("should load without error", () => {
|
||||
const component = new TimesheetStartRunning(
|
||||
component = new TimesheetStartRunning(
|
||||
containerEl,
|
||||
timekeep,
|
||||
settings,
|
||||
|
|
@ -52,4 +57,107 @@ describe("TimesheetStartRunning", () => {
|
|||
);
|
||||
component.load();
|
||||
});
|
||||
|
||||
it("should be able to stop the entry", () => {
|
||||
const start = moment();
|
||||
const end = moment().add(1, "hour");
|
||||
vi.setSystemTime(end.toDate());
|
||||
|
||||
const id = v4();
|
||||
|
||||
timekeep.setState({
|
||||
entries: [
|
||||
{
|
||||
id,
|
||||
name: "Test",
|
||||
startTime: moment(start),
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
component = new TimesheetStartRunning(
|
||||
containerEl,
|
||||
timekeep,
|
||||
settings,
|
||||
entry,
|
||||
onStartEditing
|
||||
);
|
||||
|
||||
const onStop = vi.spyOn(component, "onStop");
|
||||
|
||||
component.load();
|
||||
|
||||
const formEl = component.wrapperEl;
|
||||
expect(formEl).not.toBeNull();
|
||||
(formEl as HTMLFormElement).dispatchEvent(
|
||||
new SubmitEvent("submit", { bubbles: true, cancelable: true })
|
||||
);
|
||||
|
||||
expect(onStop).toHaveBeenCalled();
|
||||
|
||||
expect(timekeep.getState()).toEqual({
|
||||
entries: [
|
||||
{
|
||||
id,
|
||||
name: "Test",
|
||||
startTime: moment(start),
|
||||
endTime: moment(end),
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("nested running entry should have a path", () => {
|
||||
const start = moment();
|
||||
const end = moment().add(1, "hour");
|
||||
vi.setSystemTime(end.toDate());
|
||||
|
||||
const entry = {
|
||||
id: v4(),
|
||||
name: "Test",
|
||||
startTime: moment(start),
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
};
|
||||
|
||||
timekeep.setState({
|
||||
entries: [
|
||||
{
|
||||
id: v4(),
|
||||
name: "Outer",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
id: v4(),
|
||||
name: "Inner",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [entry],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
component = new TimesheetStartRunning(
|
||||
containerEl,
|
||||
timekeep,
|
||||
settings,
|
||||
entry,
|
||||
onStartEditing
|
||||
);
|
||||
|
||||
component.load();
|
||||
|
||||
const pathEls = component.wrapperEl!.querySelectorAll(".timekeep-path-to-entry__segment");
|
||||
expect(pathEls.length).toBe(3);
|
||||
|
||||
expect(pathEls.item(0).textContent).toBe("Outer >");
|
||||
expect(pathEls.item(1).textContent).toBe("Inner >");
|
||||
expect(pathEls.item(2).textContent).toBe("Test");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -131,12 +131,12 @@ export class TimesheetStartRunning extends DomComponent {
|
|||
pathEl.empty();
|
||||
|
||||
const pathToEntry = getPathToEntry(timekeep.entries, currentEntry);
|
||||
if (pathToEntry && pathToEntry.length > 0) {
|
||||
for (let i = 0; i < pathToEntry.length; i++) {
|
||||
const path = pathToEntry[i];
|
||||
const text = `${path.name} ${i < pathToEntry.length - 1 ? " >" : ""}`;
|
||||
pathEl.createSpan({ cls: "timekeep-path-to-entry__segment", text });
|
||||
}
|
||||
assert(pathToEntry, "Entry path should exist");
|
||||
|
||||
for (let i = 0; i < pathToEntry.length; i++) {
|
||||
const path = pathToEntry[i];
|
||||
const text = `${path.name}${i < pathToEntry.length - 1 ? " >" : ""}`;
|
||||
pathEl.createSpan({ cls: "timekeep-path-to-entry__segment", text });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue