Skip to content

Commit

Permalink
Release 0.8.0 (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
b263 authored Feb 4, 2024
1 parent aef9f1a commit 9f9930e
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 30 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

DistributionTool

.idea
.DS_Store
/*.svg

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Time tracker plugin for Elgato Stream Deck

[![Download](https://img.shields.io/badge/Download-v0.7.0-yellow?style=for-the-badge)](https://github.com/b263/stream-deck-time-tracker/releases/latest/download/dev.b263.time-tracker.streamDeckPlugin)
[![Download](https://img.shields.io/badge/Download-v0.8.0-yellow?style=for-the-badge)](https://github.com/b263/stream-deck-time-tracker/releases/latest/download/dev.b263.time-tracker.streamDeckPlugin)
![Status](https://img.shields.io/badge/Release_status-beta-red?style=for-the-badge)
![Last commit](https://img.shields.io/github/last-commit/b263/stream-deck-time-tracker/main?style=for-the-badge)

Expand Down
2 changes: 1 addition & 1 deletion src/dev.b263.time-tracker.sdPlugin/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"Description": "Time tracker using the Tracking Time platform. Requires a free account.",
"Icon": "assets/category",
"URL": "https://github.com/b263/stream-deck-time-tracker/issues",
"Version": "0.6.0",
"Version": "0.8.0",
"CodePath": "app/app.html",
"Software": {
"MinimumVersion": "5.0"
Expand Down
16 changes: 16 additions & 0 deletions src/js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"node-jq": "^4.2.2",
"prettier": "^3.2.5",
"rollup": "^4.9.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
Expand Down
9 changes: 7 additions & 2 deletions src/js/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { initTrackerAction } from "./lib/action/tracker-action";
import { KimaiApi } from "./lib/api/kimai-api";
import { AppEvent, StateKey } from "./lib/constants";
import { registerGlobalErrorReporter } from "./lib/error-reporter";
import { Store } from "./lib/store/store";
import { AppState, GlobalSettings } from "./lib/types";
import { AppState, GlobalSettings, SDConnectionInfo } from "./lib/types";

const store = new Store<AppState>();

$SD.onConnected(() => {
$SD.onConnected((args: SDConnectionInfo) => {
registerGlobalErrorReporter(args);
$SD.getGlobalSettings();
$SD.onDidReceiveGlobalSettings(
(event: { payload: { settings: GlobalSettings } }) => {
Expand All @@ -23,6 +25,9 @@ $SD.onConnected(() => {
}
}
);
// setTimeout(() => {
// throw new Error("test error " + new Date().toISOString());
// }, 1000);
});

EventEmitter.on(AppEvent.actionSuccess, (context: string) =>
Expand Down
4 changes: 1 addition & 3 deletions src/js/src/lib/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ export type ApiResponse<T> =

export type TrackingItem = {
id: string | number;
};

export type TimeEntry = {
duration: number;
begin: string;
};

export type Category = {
Expand Down
32 changes: 31 additions & 1 deletion src/js/src/lib/api/kimai-api-tracker-connector.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { parse } from "date-fns";
import { AppEvent, StateKey } from "../constants";
import { Store } from "../store/store";
import { Tracker, TrackerEvent } from "../tracker";
import { AppState, KimaiBackendProviderPluginConfig } from "../types";
import { ApiTrackerConnector } from "./api";
import { KimaiApi } from "./kimai-api";
import { KimaiApi, kimaiDateFormatTz } from "./kimai-api";

export class KimaiApiTrackerConnector implements ApiTrackerConnector {
#api: KimaiApi;
Expand All @@ -13,6 +14,7 @@ export class KimaiApiTrackerConnector implements ApiTrackerConnector {
onStart: this.onStart.bind(this),
onStop: this.onStop.bind(this),
onRequestWorkedToday: this.onRequestWorkedToday.bind(this),
onCheckIfCurrentlyActive: this.onCheckIfCurrentlyActive.bind(this),
};

backendProvider = "kimai" as const;
Expand Down Expand Up @@ -70,6 +72,30 @@ export class KimaiApiTrackerConnector implements ApiTrackerConnector {
}
}

async onCheckIfCurrentlyActive() {
console.log(
"KimaiApiTrackerConnector.onCheckIfCurrentlyActive()",
this.settings(this.#tracker)
);
const projectId = this.settings(this.#tracker)?.projectId;
const activityId = this.settings(this.#tracker)?.activityId;
if (this.#tracker.running) {
throw new Error(
"Tracker has already been started in Stream Deck. Check if there's an active event in the backend is too late."
);
}
if (projectId && activityId) {
const event = await this.#api.getCurrentlyActive(projectId, activityId);
if (event) {
this.#store.patchState({
[StateKey.currentEvent]: event,
});
const startTime = parse(event.begin, kimaiDateFormatTz, new Date());
this.#tracker.start(startTime);
}
}
}

connect(tracker: Tracker) {
this.#tracker = tracker;

Expand All @@ -79,6 +105,10 @@ export class KimaiApiTrackerConnector implements ApiTrackerConnector {
TrackerEvent.requestWorkedToday,
this.#bound.onRequestWorkedToday
);
tracker.addEventListener(
TrackerEvent.checkIfCurrentlyActive,
this.#bound.onCheckIfCurrentlyActive
);

return this;
}
Expand Down
184 changes: 184 additions & 0 deletions src/js/src/lib/api/kimai-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { format } from "date-fns";
import { KimaiApi, ApiConfig, kimaiDateFormat } from "./kimai-api";
import { KimaiBackendProviderPluginConfig } from "../types";

describe("KimaiApi", () => {
let api: KimaiApi;
let config: ApiConfig;

beforeEach(() => {
KimaiApi.instance = null;
api = KimaiApi.get();
config = {
url: "https://www.example.com",
user: "user",
token: "token",
};
});

test("config() should set the config if provided", () => {
KimaiApi.config(config);
expect(api.config).toEqual(config);
});

test("config() should set the config to emptyConfig if null is provided", () => {
KimaiApi.config(null as unknown as ApiConfig);
expect(api.config).toEqual(KimaiApi.emptyConfig);
});

describe("get()", () => {
test("should return an instance of KimaiApi", () => {
const instance = KimaiApi.get();
expect(instance).toBeInstanceOf(KimaiApi);
});

test("should always return the same instance", () => {
const instance1 = KimaiApi.get();
const instance2 = KimaiApi.get();
expect(instance1).toBe(instance2);
});

test("should create a new instance if one does not already exist", () => {
const instance = KimaiApi.get();
expect(instance).toBeInstanceOf(KimaiApi);
});
});

test("getCurrentUser() should fetch the current user data", async () => {
const baseUrl = "https://www.example.com/";
const user = "user";
const token = "token";
const expectedUrl = `${baseUrl}api/users/me`;
const expectedHeaders = {
"X-AUTH-USER": user,
"X-AUTH-TOKEN": token,
};
const expectedResponse = { name: "John Doe", email: "john@example.com" };
const mockFetch = jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue(expectedResponse),
});
global.fetch = mockFetch;
const result = await KimaiApi.getCurrentUser(baseUrl, user, token);
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
headers: expectedHeaders,
});
expect(result).toEqual(expectedResponse);
});

test("getCurrentUser() should return null if an error occurs", async () => {
const baseUrl = "https://www.example.com/";
const user = "user";
const token = "token";
const expectedUrl = `${baseUrl}api/users/me`;
const mockFetch = jest.fn().mockRejectedValue(new Error("Network error"));
global.fetch = mockFetch;
const result = await KimaiApi.getCurrentUser(baseUrl, user, token);
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
headers: {
"X-AUTH-USER": user,
"X-AUTH-TOKEN": token,
},
});
expect(result).toBeNull();
});

test("config setter should correctly set the config", () => {
const config = {
url: "https://www.example.com/",
user: "user",
token: "token",
};
api.config = config;
expect(api.config).toEqual(config);
});

test("config getter should correctly get the config", () => {
const config = {
url: "https://www.example.com/",
user: "user",
token: "token",
};
api.config = config;
const retrievedConfig = api.config;
expect(retrievedConfig).toEqual(config);
});

test("config getter should return default values when config is not set", () => {
const defaultConfig = { url: "", user: "", token: "" };
expect(api.config).toEqual(defaultConfig);
});

test("fetchOptions should return correct headers", () => {
const config = {
url: "https://www.example.com/",
user: "user",
token: "token",
};
api.config = config;
const expectedHeaders = {
"X-AUTH-USER": config.user,
"X-AUTH-TOKEN": config.token,
"Content-Type": "application/json",
};
expect(api.fetchOptions).toEqual({ headers: expectedHeaders });
});

test("assertValidConfig should throw error when user is not set", () => {
const config = {
url: "https://www.example.com/",
user: "",
token: "token",
};
api.config = config;
expect(() => api.assertValidConfig()).toThrow(
"Invalid config. User must not be empty."
);
});

test("assertValidConfig should throw error when baseUrl is not set", () => {
const config = { url: "", user: "user", token: "token" };
api.config = config;
expect(() => api.assertValidConfig()).toThrow(
"Invalid config. BaseUrl must not be empty."
);
});

test("assertValidConfig should throw error when token is not set", () => {
const config = { url: "https://www.example.com/", user: "user", token: "" };
api.config = config;
expect(() => api.assertValidConfig()).toThrow(
"Invalid config. Token must not be empty."
);
});

test("startTracking should send a POST request with correct parameters", async () => {
const config = {
url: "https://www.example.com/",
user: "user",
token: "token",
};
api.config = config;
const mockFetch = jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue({}),
});
global.fetch = mockFetch;
const trackingConfig = { projectId: 1, activityId: 2 };
await api.startTracking(trackingConfig as KimaiBackendProviderPluginConfig);
const expectedUrl = `${config.url}api/timesheets`;
const expectedOptions = {
headers: {
"X-AUTH-USER": config.user,
"X-AUTH-TOKEN": config.token,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
begin: format(new Date(), kimaiDateFormat),
project: trackingConfig.projectId,
activity: trackingConfig.activityId,
description: "",
}),
};
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
});
});
Loading

0 comments on commit 9f9930e

Please sign in to comment.