mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
1591 lines
32 KiB
TypeScript
1591 lines
32 KiB
TypeScript
import moment from "moment";
|
|
|
|
import { Timekeep, TimeEntry, TimeEntryGroup } from "./schema";
|
|
import { defaultSettings, TimekeepSettings } from "./settings";
|
|
import {
|
|
load,
|
|
LoadError,
|
|
withEntry,
|
|
LoadSuccess,
|
|
createEntry,
|
|
removeEntry,
|
|
updateEntry,
|
|
withSubEntry,
|
|
isKeepRunning,
|
|
isEntryRunning,
|
|
getRunningEntry,
|
|
getEntryDuration,
|
|
getTotalDuration,
|
|
getEntriesOrdered,
|
|
setEntryCollapsed,
|
|
getUniqueEntryHash,
|
|
stopRunningEntries,
|
|
replaceTimekeepCodeblock,
|
|
extractTimekeepCodeblocks,
|
|
} from "./timekeep";
|
|
|
|
/**
|
|
* Generates a code block surrounding the provided JSON
|
|
* with the provided leading and trailing number of lines
|
|
*
|
|
* @param json The JSON to put between the codeblocks
|
|
* @param linesBefore Number of lines before the codeblock
|
|
* @param linesAfter Number of lines after the codeblock
|
|
* @returns The generated codeblock
|
|
*/
|
|
const createCodeBlock = (
|
|
json: string,
|
|
linesBefore: number,
|
|
linesAfter: number
|
|
) => {
|
|
let output = "";
|
|
for (let i = 0; i < linesBefore; i++) {
|
|
output += "\n";
|
|
}
|
|
output += "```timekeep\n";
|
|
output += json;
|
|
output += "\n```";
|
|
for (let i = 0; i < linesAfter; i++) {
|
|
output += "\n";
|
|
}
|
|
return output;
|
|
};
|
|
|
|
describe("replacing content", () => {
|
|
it("should replace codeblock contents", () => {
|
|
const lineStart = 4; // Line the codeblock should start on
|
|
const lineEnd = lineStart + 2; // Line the codeblock should end on
|
|
|
|
// Input data to replace
|
|
const input = createCodeBlock(
|
|
`{"entries":[{"name":"Block 1","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
|
|
4,
|
|
4
|
|
);
|
|
|
|
// Timekeep with a renamed block
|
|
const inputTimekeep: Timekeep = {
|
|
entries: [
|
|
{
|
|
name: "Block 2",
|
|
startTime: moment("2024-03-17T01:33:51.630Z"),
|
|
endTime: moment("2024-03-17T01:33:55.151Z"),
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
// Value with the renamed block
|
|
const expected = createCodeBlock(
|
|
`{"entries":[{"name":"Block 2","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
|
|
4,
|
|
4
|
|
);
|
|
|
|
const output = replaceTimekeepCodeblock(
|
|
inputTimekeep,
|
|
input,
|
|
lineStart,
|
|
lineEnd
|
|
);
|
|
|
|
expect(output).toBe(expected);
|
|
});
|
|
|
|
it("should fail if codeblock is missing", () => {
|
|
const input = createCodeBlock("", 4, 4);
|
|
// Start not code fences
|
|
expect(() =>
|
|
replaceTimekeepCodeblock({ entries: [] }, input, 2, 4)
|
|
).toThrow();
|
|
|
|
// End not code fences
|
|
expect(() =>
|
|
replaceTimekeepCodeblock({ entries: [] }, input, 4, 8)
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("loading timekeep", () => {
|
|
it("should give empty timekeep when given empty string", () => {
|
|
const result = load("");
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const successResult = result as LoadSuccess;
|
|
|
|
// Ensure the contents match
|
|
expect(successResult.timekeep).toEqual({ entries: [] });
|
|
});
|
|
|
|
it("should load valid timekeep successfully", () => {
|
|
const data = `{"entries":[{"name":"Block 1","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null},{"name":"Block 2","startTime":"2024-03-17T01:33:51.630Z","endTime":null,"subEntries":null},{"name":"Non Started Block","startTime":null,"endTime":null,"subEntries":null}]}`;
|
|
const expected = {
|
|
entries: [
|
|
{
|
|
name: "Block 1",
|
|
startTime: moment("2024-03-17T01:33:51.630Z"),
|
|
endTime: moment("2024-03-17T01:33:55.151Z"),
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 2",
|
|
startTime: moment("2024-03-17T01:33:51.630Z"),
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Non Started Block",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = load(data);
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const successResult = result as LoadSuccess;
|
|
|
|
// Ensure the contents match
|
|
expect(successResult.timekeep).toEqual(expected);
|
|
});
|
|
|
|
it("should give error on invalid timekeep (JSON)", () => {
|
|
const data = "{";
|
|
|
|
const result = load(data);
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
const errorResult = result as LoadError;
|
|
|
|
expect(errorResult.error).toBe("Failed to parse timekeep JSON");
|
|
});
|
|
|
|
it("should give error on invalid timekeep (validation)", () => {
|
|
const data = `{"entries":[{"startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`;
|
|
|
|
const result = load(data);
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("manipulating entries", () => {
|
|
describe("update entry", () => {
|
|
it("updating existing entry should succeed", () => {
|
|
const currentTime = moment();
|
|
const entryToUpdate = {
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const updatedEntry = {
|
|
name: "Block 1 Updated",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const entries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 3",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
entryToUpdate,
|
|
{
|
|
name: "Block 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const expectedEntries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 3",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
updatedEntry,
|
|
{
|
|
name: "Block 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const updated = updateEntry(entries, entryToUpdate, updatedEntry);
|
|
expect(updated).toEqual(expectedEntries);
|
|
});
|
|
});
|
|
|
|
describe("remove entry", () => {
|
|
it("attempting to remove sub entry on non group should do nothing", () => {
|
|
const currentTime = moment();
|
|
|
|
const parent = {
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
const entryToRemove = {
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const output = removeEntry([parent], entryToRemove);
|
|
expect(output).toEqual([parent]);
|
|
});
|
|
|
|
it("remove on single entry should stay same if not target", () => {
|
|
const currentTime = moment();
|
|
const entryToRemove = {
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const entries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 2",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Block 3",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 3",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
entryToRemove,
|
|
],
|
|
},
|
|
];
|
|
|
|
const expectedEntries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 2",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Block 3",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 3",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const updated = removeEntry(entries, entryToRemove);
|
|
expect(updated).toEqual(expectedEntries);
|
|
});
|
|
|
|
it("should be able to remove entry", () => {
|
|
const currentTime = moment();
|
|
const entryToRemove = {
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const entries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
entryToRemove,
|
|
{
|
|
name: "Block 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const expectedEntries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const updated = removeEntry(entries, entryToRemove);
|
|
expect(updated).toEqual(expectedEntries);
|
|
});
|
|
|
|
it("should be able to remove nested entry", () => {
|
|
const currentTime = moment();
|
|
const entryToRemove = {
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const entries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1 A",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
entryToRemove,
|
|
],
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const expectedEntries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1 A",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const updated = removeEntry(entries, entryToRemove);
|
|
expect(updated).toEqual(expectedEntries);
|
|
});
|
|
|
|
it("should collapse groups with only one entry on remove", () => {
|
|
const currentTime = moment();
|
|
const entryToRemove = {
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const entries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1 A",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
entryToRemove,
|
|
],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const expectedEntries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1 A",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const updated = removeEntry(entries, entryToRemove);
|
|
expect(updated).toEqual(expectedEntries);
|
|
});
|
|
|
|
it("should collapse groups with only one entry on remove (single)", () => {
|
|
const currentTime = moment();
|
|
const entryToRemove = {
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const entries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1 A",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
entryToRemove,
|
|
],
|
|
},
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const expectedEntries = [
|
|
{
|
|
name: "Block 3",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const updated = removeEntry(entries, entryToRemove);
|
|
expect(updated).toEqual(expectedEntries);
|
|
});
|
|
});
|
|
|
|
describe("adding entry", () => {
|
|
it("creating entry should use current time", () => {
|
|
const currentTime = moment();
|
|
|
|
const entry = createEntry("Block 1", currentTime);
|
|
expect(entry).toStrictEqual({
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
});
|
|
});
|
|
|
|
it("should add new entry to entries", () => {
|
|
const currentTime = moment();
|
|
|
|
const input = [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const expected = [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "New Entry",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const output = withEntry(input, "New Entry", currentTime);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it("should generate block name when empty", () => {
|
|
const currentTime = moment();
|
|
|
|
const input = [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const expected = [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 2",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const output = withEntry(input, "", currentTime);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("adding sub entry", () => {
|
|
it("adding first entry should convert to group", () => {
|
|
const currentTime = moment();
|
|
|
|
const input = {
|
|
name: "Entry",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const expected = {
|
|
name: "Entry",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "New Entry",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const output = withSubEntry(input, "New Entry", currentTime);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it("adding to group should extend sub entries", () => {
|
|
const currentTime = moment();
|
|
|
|
const input = {
|
|
name: "Entry",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const expected = {
|
|
name: "Entry",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "New Entry",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const output = withSubEntry(input, "New Entry", currentTime);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it("empty name should generate a part name (single)", () => {
|
|
const currentTime = moment();
|
|
|
|
const input = {
|
|
name: "Entry",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const expected = {
|
|
name: "Entry",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const output = withSubEntry(input, "", currentTime);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it("empty name should generate a part name (group)", () => {
|
|
const currentTime = moment();
|
|
|
|
const input = {
|
|
name: "Entry",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const expected = {
|
|
name: "Entry",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 3",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const output = withSubEntry(input, "", currentTime);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("stopping entries", () => {
|
|
it("should stop running entries", () => {
|
|
const currentTime = moment();
|
|
const endTime = moment().add(15, "hours");
|
|
|
|
const input = [
|
|
{
|
|
name: "Entry",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Running Entry",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Running Entry",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const expected = [
|
|
{
|
|
name: "Entry",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Running Entry",
|
|
startTime: currentTime,
|
|
endTime: endTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Running Entry",
|
|
startTime: currentTime,
|
|
endTime: endTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const output = stopRunningEntries(input, endTime);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("checking entries", () => {
|
|
it("should show timekeep running", () => {});
|
|
|
|
it("should show timekeep stopped", () => {});
|
|
|
|
it("should determine entry running state", () => {
|
|
const currentTime = moment();
|
|
expect(
|
|
isEntryRunning({
|
|
name: "Running Entry",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
})
|
|
).toBe(true);
|
|
|
|
expect(
|
|
isEntryRunning({
|
|
name: "Stopped Entry",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("should determine entry running state (nested)", () => {
|
|
const currentTime = moment();
|
|
expect(
|
|
isEntryRunning({
|
|
name: "Running Entry",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Running Entry",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
})
|
|
).toBe(true);
|
|
|
|
expect(
|
|
isEntryRunning({
|
|
name: "Stopped Entry",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Stopped Entry",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("should find running entry", () => {
|
|
const currentTime = moment();
|
|
const runningEntry = {
|
|
name: "Running Entry",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
};
|
|
|
|
const input = [
|
|
{
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
runningEntry,
|
|
];
|
|
|
|
const output = getRunningEntry(input);
|
|
|
|
expect(output).toBe(runningEntry);
|
|
});
|
|
|
|
it("should find nested running entry", () => {
|
|
const currentTime = moment();
|
|
const runningEntry = {
|
|
name: "Running Entry",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
};
|
|
|
|
const input = [
|
|
{
|
|
name: "Block 1",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
runningEntry,
|
|
],
|
|
},
|
|
];
|
|
|
|
const output = getRunningEntry(input);
|
|
|
|
expect(output).toBe(runningEntry);
|
|
});
|
|
|
|
it("should not find running entry", () => {
|
|
const currentTime = moment();
|
|
|
|
const input = [
|
|
{
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 2",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const output = getRunningEntry(input);
|
|
|
|
expect(output).toBe(null);
|
|
});
|
|
|
|
it("should show keep running", () => {
|
|
const currentTime = moment();
|
|
const runningEntry = {
|
|
name: "Running Entry",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
};
|
|
|
|
const input: Timekeep = {
|
|
entries: [
|
|
{
|
|
name: "Block 1",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
runningEntry,
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(isKeepRunning(input)).toBe(true);
|
|
});
|
|
|
|
it("should show keep not running", () => {
|
|
const currentTime = moment();
|
|
|
|
const input: Timekeep = {
|
|
entries: [
|
|
{
|
|
name: "Block 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Block 2",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(isKeepRunning(input)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("ordering entries", () => {
|
|
it("should be in reverse order", () => {
|
|
const currentTime = moment();
|
|
|
|
const input = [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const expected = [
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const settings: TimekeepSettings = defaultSettings;
|
|
settings.reverseSegmentOrder = true;
|
|
|
|
const output = getEntriesOrdered(input, settings);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it("order should not change", () => {
|
|
const currentTime = moment();
|
|
|
|
const input = [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const expected = [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
];
|
|
|
|
const settings: TimekeepSettings = defaultSettings;
|
|
settings.reverseSegmentOrder = false;
|
|
|
|
const output = getEntriesOrdered(input, settings);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("hashing", () => {
|
|
it("hash should match when content matches", () => {
|
|
const currentTime = moment();
|
|
|
|
const left: TimeEntry = {
|
|
name: "Test",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const right: TimeEntry = {
|
|
name: "Test",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 2",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(getUniqueEntryHash(left)).toBe(getUniqueEntryHash(right));
|
|
});
|
|
|
|
it("hash shouldn't match when content doesn't", () => {
|
|
const currentTime = moment();
|
|
|
|
const left: TimeEntry = {
|
|
name: "Test",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const right: TimeEntry = {
|
|
name: "Test",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part 1",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(getUniqueEntryHash(left)).not.toBe(getUniqueEntryHash(right));
|
|
});
|
|
});
|
|
|
|
describe("duration", () => {
|
|
it("should get entry duration", () => {
|
|
const currentTime = moment();
|
|
const durationMs = 500;
|
|
|
|
const input: TimeEntry = {
|
|
name: "Test",
|
|
startTime: currentTime,
|
|
endTime: currentTime.clone().add(durationMs, "ms"),
|
|
subEntries: null,
|
|
};
|
|
|
|
const output = getEntryDuration(input, currentTime);
|
|
|
|
expect(output).toBe(durationMs);
|
|
});
|
|
|
|
it("duration of non started entry should be zero", () => {
|
|
const currentTime = moment();
|
|
const durationMs = 0;
|
|
|
|
const input: TimeEntry = {
|
|
name: "Test",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: null,
|
|
};
|
|
|
|
const output = getEntryDuration(input, currentTime);
|
|
|
|
expect(output).toBe(durationMs);
|
|
});
|
|
|
|
it("duration should include children", () => {
|
|
const currentTime = moment();
|
|
const durationMs = 500;
|
|
|
|
const input: TimeEntry = {
|
|
name: "Test",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part A",
|
|
startTime: currentTime,
|
|
endTime: currentTime.clone().add(durationMs, "ms"),
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part B",
|
|
startTime: currentTime,
|
|
endTime: currentTime.clone().add(durationMs, "ms"),
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part c",
|
|
startTime: currentTime,
|
|
endTime: currentTime.clone().add(durationMs, "ms"),
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const expected = durationMs * 3;
|
|
|
|
const output = getEntryDuration(input, currentTime);
|
|
|
|
expect(output).toBe(expected);
|
|
});
|
|
|
|
it("duration should use current as end for unfinished entries", () => {
|
|
const currentTime = moment();
|
|
const durationMs = 500;
|
|
const endTime = currentTime.clone().add(durationMs, "ms");
|
|
|
|
const input: TimeEntry = {
|
|
name: "Test",
|
|
startTime: currentTime,
|
|
endTime: null,
|
|
subEntries: null,
|
|
};
|
|
|
|
const output = getEntryDuration(input, endTime);
|
|
|
|
expect(output).toBe(durationMs);
|
|
});
|
|
|
|
it("should get total duration", () => {
|
|
const currentTime = moment();
|
|
const durationMs = 500;
|
|
|
|
const input: TimeEntry[] = [
|
|
{
|
|
name: "Test",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part A",
|
|
startTime: currentTime,
|
|
endTime: currentTime.clone().add(durationMs, "ms"),
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part B",
|
|
startTime: currentTime,
|
|
endTime: currentTime.clone().add(durationMs, "ms"),
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part c",
|
|
startTime: currentTime,
|
|
endTime: currentTime.clone().add(durationMs, "ms"),
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Test",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Part A",
|
|
startTime: currentTime,
|
|
endTime: currentTime.clone().add(durationMs, "ms"),
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part B",
|
|
startTime: currentTime,
|
|
endTime: currentTime.clone().add(durationMs, "ms"),
|
|
subEntries: null,
|
|
},
|
|
{
|
|
name: "Part c",
|
|
startTime: currentTime,
|
|
endTime: currentTime.clone().add(durationMs, "ms"),
|
|
subEntries: null,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const expected = durationMs * 6;
|
|
|
|
const output = getTotalDuration(input, currentTime);
|
|
|
|
expect(output).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("extracting code blocks", () => {
|
|
it("should extract codeblock contents", () => {
|
|
// Input data to find
|
|
const input1 = createCodeBlock(
|
|
`{"entries":[{"name":"Block 1","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
|
|
4,
|
|
4
|
|
);
|
|
|
|
// Input data to find
|
|
const input2 = createCodeBlock(
|
|
`{"entries":[{"name":"Block 2","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
|
|
4,
|
|
4
|
|
);
|
|
|
|
// Timekeep with a renamed block
|
|
const inputTimekeep1: Timekeep = {
|
|
entries: [
|
|
{
|
|
name: "Block 1",
|
|
startTime: moment("2024-03-17T01:33:51.630Z"),
|
|
endTime: moment("2024-03-17T01:33:55.151Z"),
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
// Timekeep with a renamed block
|
|
const inputTimekeep2: Timekeep = {
|
|
entries: [
|
|
{
|
|
name: "Block 2",
|
|
startTime: moment("2024-03-17T01:33:51.630Z"),
|
|
endTime: moment("2024-03-17T01:33:55.151Z"),
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const text = "\n\n\n" + input1 + "\n\n\n\n" + input2;
|
|
|
|
const output = extractTimekeepCodeblocks(text);
|
|
|
|
expect(output[0]).toStrictEqual(inputTimekeep1);
|
|
expect(output[1]).toStrictEqual(inputTimekeep2);
|
|
expect(output.length).toBe(2);
|
|
});
|
|
|
|
it("should ignore codeblocks that are not closed", () => {
|
|
// Input data to find
|
|
const input1 = createCodeBlock(
|
|
`{"entries":[{"name":"Block 1","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
|
|
4,
|
|
4
|
|
);
|
|
|
|
// Timekeep with a renamed block
|
|
const inputTimekeep1: Timekeep = {
|
|
entries: [
|
|
{
|
|
name: "Block 1",
|
|
startTime: moment("2024-03-17T01:33:51.630Z"),
|
|
endTime: moment("2024-03-17T01:33:55.151Z"),
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const text = "\n\n\n" + input1 + "\n\n\n\n```timekeep\n\n";
|
|
|
|
const output = extractTimekeepCodeblocks(text);
|
|
|
|
expect(output[0]).toStrictEqual(inputTimekeep1);
|
|
expect(output.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("collapse state", () => {
|
|
it("should update group collapse state when set to true", () => {
|
|
const currentTime = moment();
|
|
|
|
const input: TimeEntryGroup = {
|
|
name: "Test",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Test",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const collapsed = setEntryCollapsed(input, true);
|
|
|
|
expect(collapsed.subEntries).not.toBeNull();
|
|
expect((collapsed as TimeEntryGroup).collapsed).toBeDefined();
|
|
expect((collapsed as TimeEntryGroup).collapsed).toBe(true);
|
|
});
|
|
|
|
it("collapsed state should be undefined when false", () => {
|
|
const currentTime = moment();
|
|
|
|
const input: TimeEntryGroup = {
|
|
name: "Test",
|
|
startTime: null,
|
|
endTime: null,
|
|
subEntries: [
|
|
{
|
|
name: "Test",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const collapsed = setEntryCollapsed(input, false);
|
|
|
|
expect(collapsed.subEntries).not.toBeNull();
|
|
expect((collapsed as TimeEntryGroup).collapsed).toBeUndefined();
|
|
});
|
|
|
|
it("should not set collapse state on single entry", () => {
|
|
const currentTime = moment();
|
|
|
|
const input: TimeEntry = {
|
|
name: "Test",
|
|
startTime: currentTime,
|
|
endTime: currentTime,
|
|
subEntries: null,
|
|
};
|
|
|
|
const collapsed = setEntryCollapsed(input, true);
|
|
|
|
expect(collapsed.subEntries).toBeNull();
|
|
expect((collapsed as TimeEntryGroup).collapsed).toBeUndefined();
|
|
});
|
|
});
|