jacobtread_obsidian-timekeep/src/components/TimesheetRow/TimesheetRowContentEditing.test.ts

383 lines
9.7 KiB
TypeScript

// @vitest-environment happy-dom
import type { App } from "obsidian";
import moment from "moment";
import { beforeEach, it, describe, Mock, vi, expect, afterEach } from "vitest";
import type { TimekeepSettings } from "@/settings";
import type { Store } from "@/store";
import { createMockContainer } from "@/__mocks__/obsidian";
import { defaultSettings } from "@/settings";
import { createStore } from "@/store";
import { TimesheetRowContentEditing } from "./TimesheetRowContentEditing";
import { defaultTimekeep, type TimeEntry, type Timekeep } from "@/timekeep/schema";
describe("TimesheetRowContentEditing", () => {
let containerEl: HTMLElement;
let app: App;
let timekeep: Store<Timekeep>;
let settings: Store<TimekeepSettings>;
let onFinishEditing: Mock<() => void>;
let component: TimesheetRowContentEditing;
beforeEach(() => {
app = {} as App;
containerEl = createMockContainer();
timekeep = createStore(defaultTimekeep());
settings = createStore(defaultSettings);
onFinishEditing = vi.fn();
});
afterEach(() => {
if (component) component.unload();
});
it("should load without error", () => {
const start = moment();
const entry: TimeEntry = {
id: 1,
name: "Test",
startTime: moment(start),
endTime: null,
subEntries: null,
};
component = new TimesheetRowContentEditing(
containerEl,
app,
timekeep,
settings,
entry,
onFinishEditing
);
component.load();
});
it("clicking the cancel button should call onFinishEditing", () => {
const start = moment();
const entry: TimeEntry = {
id: 1,
name: "Test",
startTime: moment(start),
endTime: null,
subEntries: null,
};
component = new TimesheetRowContentEditing(
containerEl,
app,
timekeep,
settings,
entry,
onFinishEditing
);
component.load();
const cancelButton = containerEl.querySelector('.timekeep-action[data-action="cancel"]');
expect(cancelButton).not.toBeNull();
(cancelButton as HTMLButtonElement).click();
expect(onFinishEditing).toHaveBeenCalledOnce();
});
it("clicking the save button should update the timekeep state and call onFinishEditing", () => {
const start = moment();
const entry: TimeEntry = {
id: 1,
name: "Test",
startTime: moment(start),
endTime: moment(start),
subEntries: null,
};
const setState = vi.spyOn(timekeep, "setState");
component = new TimesheetRowContentEditing(
containerEl,
app,
timekeep,
settings,
entry,
onFinishEditing
);
const onSubmit = vi.spyOn(component, "onSubmit");
component.load();
const form = containerEl.querySelector("form.timekeep-editing");
expect(form).not.toBeNull();
(form as HTMLFormElement).dispatchEvent(
new SubmitEvent("submit", { bubbles: true, cancelable: true })
);
expect(onSubmit).toHaveBeenCalledOnce();
expect(onFinishEditing).toHaveBeenCalledOnce();
expect(setState).toHaveBeenCalledOnce();
});
it("clicking the save button on a group should update the timekeep state and call onFinishEditing", () => {
const entry: TimeEntry = {
id: 1,
name: "Test",
startTime: null,
endTime: null,
subEntries: [],
};
const setState = vi.spyOn(timekeep, "setState");
component = new TimesheetRowContentEditing(
containerEl,
app,
timekeep,
settings,
entry,
onFinishEditing
);
const onSubmit = vi.spyOn(component, "onSubmit");
component.load();
const form = containerEl.querySelector("form.timekeep-editing");
expect(form).not.toBeNull();
(form as HTMLFormElement).dispatchEvent(
new SubmitEvent("submit", { bubbles: true, cancelable: true })
);
expect(onSubmit).toHaveBeenCalledOnce();
expect(onFinishEditing).toHaveBeenCalledOnce();
expect(setState).toHaveBeenCalledOnce();
});
it("clicking the save button on a unstarted entry should update the timekeep state and call onFinishEditing", () => {
const entry: TimeEntry = {
id: 1,
name: "Test",
startTime: null,
endTime: null,
subEntries: null,
};
const setState = vi.spyOn(timekeep, "setState");
component = new TimesheetRowContentEditing(
containerEl,
app,
timekeep,
settings,
entry,
onFinishEditing
);
const onSubmit = vi.spyOn(component, "onSubmit");
component.load();
const form = containerEl.querySelector("form.timekeep-editing");
expect(form).not.toBeNull();
(form as HTMLFormElement).dispatchEvent(
new SubmitEvent("submit", { bubbles: true, cancelable: true })
);
expect(onSubmit).toHaveBeenCalledOnce();
expect(onFinishEditing).toHaveBeenCalledOnce();
expect(setState).toHaveBeenCalledOnce();
});
it("editing a group entry should hide the start and end time inputs", () => {
const entry: TimeEntry = {
id: 1,
name: "Test",
startTime: null,
endTime: null,
subEntries: [],
};
component = new TimesheetRowContentEditing(
containerEl,
app,
timekeep,
settings,
entry,
onFinishEditing
);
component.load();
const startTime =
containerEl.querySelector('.timekeep-input[name="start-time"]')?.parentElement ?? null;
const endTime =
containerEl.querySelector('.timekeep-input[name="end-time"]')?.parentElement ?? null;
expect(startTime).not.toBeNull();
expect(endTime).not.toBeNull();
expect((startTime as HTMLElement).hidden).toBeTruthy();
expect((endTime as HTMLElement).hidden).toBeTruthy();
});
it("clicking delete on an entry should open a modal for confirmation", () => {
const entry: TimeEntry = {
id: 1,
name: "Test",
startTime: null,
endTime: null,
subEntries: [],
};
component = new TimesheetRowContentEditing(
containerEl,
app,
timekeep,
settings,
entry,
onFinishEditing
);
const onConfirmDelete = vi.spyOn(component, "onConfirmDelete");
component.load();
const deleteButton = containerEl.querySelector('.timekeep-action[data-action="delete"]');
expect(deleteButton).not.toBeNull();
(deleteButton as HTMLButtonElement).click();
expect(onConfirmDelete).toHaveBeenCalled();
const contentEl: HTMLElement = document!.querySelector(".mock-modal-content")!;
expect(contentEl).toBeInstanceOf(HTMLElement);
});
it("cancelling deletion should do nothing", () => {
const entry: TimeEntry = {
id: 1,
name: "Test",
startTime: null,
endTime: null,
subEntries: [],
};
timekeep.setState({ entries: [entry] });
component = new TimesheetRowContentEditing(
containerEl,
app,
timekeep,
settings,
entry,
onFinishEditing
);
const onConfirmDelete = vi.spyOn(component, "onConfirmDelete");
const onConfirmedDelete = vi.spyOn(component, "onConfirmedDelete");
component.load();
const deleteButton = containerEl.querySelector('.timekeep-action[data-action="delete"]');
expect(deleteButton).not.toBeNull();
(deleteButton as HTMLButtonElement).click();
expect(onConfirmDelete).toHaveBeenCalled();
const contentEl: HTMLElement = document!.querySelector(".mock-modal-content")!;
expect(contentEl).toBeInstanceOf(HTMLElement);
const cancelButton = contentEl.querySelector(
'.timekeep-confirm-modal-button[data-action="cancel"]'
);
expect(cancelButton).not.toBeNull();
(cancelButton as HTMLButtonElement).click();
expect(onConfirmedDelete).toHaveBeenCalledWith(false);
// Timekeep should not be empty
expect(timekeep.getState()).toEqual({ entries: [entry] });
});
it("confirming deletion should remove the entry from the timekeep", () => {
const entry: TimeEntry = {
id: 1,
name: "Test",
startTime: null,
endTime: null,
subEntries: [],
};
timekeep.setState({ entries: [entry] });
component = new TimesheetRowContentEditing(
containerEl,
app,
timekeep,
settings,
entry,
onFinishEditing
);
const onConfirmDelete = vi.spyOn(component, "onConfirmDelete");
const onConfirmedDelete = vi.spyOn(component, "onConfirmedDelete");
component.load();
const deleteButton = containerEl.querySelector('.timekeep-action[data-action="delete"]');
expect(deleteButton).not.toBeNull();
(deleteButton as HTMLButtonElement).click();
expect(onConfirmDelete).toHaveBeenCalled();
const contentEl: HTMLElement = document!.querySelector(".mock-modal-content")!;
expect(contentEl).toBeInstanceOf(HTMLElement);
const okButton = contentEl.querySelector(
'.timekeep-confirm-modal-button[data-action="ok"]'
);
expect(okButton).not.toBeNull();
(okButton as HTMLButtonElement).click();
expect(onConfirmedDelete).toHaveBeenCalledWith(true);
// Timekeep should be empty
expect(timekeep.getState()).toEqual({ entries: [] });
});
it("editing a group entry should hide the start and end time inputs", () => {
const start = moment();
const entry: TimeEntry = {
id: 1,
name: "Test",
startTime: moment(start),
endTime: moment(start),
subEntries: null,
};
timekeep.setState({ entries: [entry] });
const setState = vi.spyOn(timekeep, "setState");
component = new TimesheetRowContentEditing(
containerEl,
app,
timekeep,
settings,
entry,
onFinishEditing
);
const onSubmit = vi.spyOn(component, "onSubmit");
component.load();
const startTime = containerEl.querySelector('.timekeep-input[name="start-time"]');
const endTime = containerEl.querySelector('.timekeep-input[name="end-time"]');
expect(startTime).not.toBeNull();
expect(endTime).not.toBeNull();
(startTime as HTMLInputElement).value = "Test";
(endTime as HTMLInputElement).value = "Test";
const form = containerEl.querySelector("form.timekeep-editing");
expect(form).not.toBeNull();
(form as HTMLFormElement).dispatchEvent(
new SubmitEvent("submit", { bubbles: true, cancelable: true })
);
expect(onSubmit).toHaveBeenCalledOnce();
expect(onFinishEditing).toHaveBeenCalledOnce();
expect(setState).toHaveBeenCalledOnce();
// Entry should not have changed if the input values were not valid timestamps
expect(timekeep.getState()).toEqual({ entries: [entry] });
});
});