diff --git a/src/App.test.tsx b/src/App.test.tsx
index 7d3223ba..d4a4013d 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -2,9 +2,11 @@ import { act, cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { App } from "./App";
-import { budgetsDB, calcHistDB, optionsDB } from "./db";
import { BudgetMother } from "./guitos/domain/budget.mother";
import { budgetContextSpy, testEmptyBudgetContext } from "./setupTests";
+import { localForageBudgetRepository } from "./guitos/infrastructure/localForageBudgetRepository";
+
+const budgetRepository = new localForageBudgetRepository();
describe("App", () => {
const comp = ;
@@ -19,15 +21,9 @@ describe("App", () => {
"Figure out where your money went, plan ahead of time and analyze past expenditures.",
),
).toBeInTheDocument();
- expect(budgetsDB.config("name")).toBe("guitos");
- expect(budgetsDB.config("storeName")).toBe("budgets");
- expect(optionsDB.config("name")).toBe("guitos");
- expect(optionsDB.config("storeName")).toBe("options");
- expect(calcHistDB.config("name")).toBe("guitos");
- expect(calcHistDB.config("storeName")).toBe("calcHistDB");
await expect(
- budgetsDB.getItem(BudgetMother.testBudget().id.toString()),
- ).resolves.toBeNull();
+ budgetRepository.get(BudgetMother.testBudget().id),
+ ).rejects.toThrow();
});
it("shows new budget when clicking new button", async () => {
@@ -42,7 +38,7 @@ describe("App", () => {
expect(await screen.findByText("Revenue")).toBeInTheDocument();
expect(await screen.findByText("Expenses")).toBeInTheDocument();
await expect(
- budgetsDB.getItem(BudgetMother.testBudget().id.toString()),
+ budgetRepository.get(BudgetMother.testBudget().id),
).resolves.toEqual(BudgetMother.testBudget());
});
@@ -50,7 +46,7 @@ describe("App", () => {
render(comp);
await act(async () => {
await expect(
- budgetsDB.getItem(BudgetMother.testBudget().id.toString()),
+ budgetRepository.get(BudgetMother.testBudget().id),
).resolves.toEqual(BudgetMother.testBudget());
});
@@ -70,8 +66,8 @@ describe("App", () => {
await act(async () => {
await expect(
- budgetsDB.getItem(BudgetMother.testBudget().id.toString()),
- ).resolves.toBeNull();
+ budgetRepository.get(BudgetMother.testBudget().id),
+ ).rejects.toThrow();
});
});
});
diff --git a/src/db.ts b/src/db.ts
deleted file mode 100644
index 394a4f5a..00000000
--- a/src/db.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import localforage from "localforage";
-
-export const budgetsDB = localforage.createInstance({
- name: "guitos",
- storeName: "budgets",
-});
-
-export const optionsDB = localforage.createInstance({
- name: "guitos",
- storeName: "options",
-});
-
-export const calcHistDB = localforage.createInstance({
- name: "guitos",
- storeName: "calcHistDB",
-});
diff --git a/src/guitos/context/ConfigContext.tsx b/src/guitos/context/ConfigContext.tsx
new file mode 100644
index 00000000..738a7641
--- /dev/null
+++ b/src/guitos/context/ConfigContext.tsx
@@ -0,0 +1,64 @@
+import {
+ type PropsWithChildren,
+ createContext,
+ useContext,
+ useState,
+} from "react";
+import type {
+ CurrencyInputProps,
+ IntlConfig,
+} from "react-currency-input-field/dist/components/CurrencyInputProps";
+import { localForageOptionsRepository } from "../infrastructure/localForageOptionsRepository";
+
+interface ConfigContextInterface {
+ intlConfig: IntlConfig | undefined;
+ setIntlConfig: (value: IntlConfig) => void;
+ currency: string;
+ setCurrency: (value: string) => void;
+}
+
+const optionsRepository = new localForageOptionsRepository();
+
+const ConfigContext = createContext({
+ intlConfig: {
+ locale: optionsRepository.getUserLang(),
+ currency: optionsRepository.getDefaultCurrencyCode(),
+ },
+ setIntlConfig: (value: IntlConfig) => value,
+ currency: optionsRepository.getDefaultCurrencyCode(),
+ setCurrency: (value: string) => value,
+});
+
+function useConfig() {
+ const { currency, setCurrency, intlConfig, setIntlConfig } =
+ useContext(ConfigContext);
+
+ function handleCurrency(c: string) {
+ setCurrency(c);
+ setIntlConfig({ locale: optionsRepository.getUserLang(), currency: c });
+ }
+
+ return { intlConfig, setIntlConfig, currency, handleCurrency };
+}
+
+function ConfigProvider({ children }: PropsWithChildren) {
+ const [currency, setCurrency] = useState(
+ optionsRepository.getDefaultCurrencyCode(),
+ );
+ const [intlConfig, setIntlConfig] = useState<
+ CurrencyInputProps["intlConfig"]
+ >({
+ locale: optionsRepository.getUserLang(),
+ currency: currency,
+ });
+
+ return (
+
+ {children}
+
+ );
+}
+
+export { ConfigProvider, useConfig };
diff --git a/src/guitos/infrastructure/localForageBudgetRepository.ts b/src/guitos/infrastructure/localForageBudgetRepository.ts
index 541c8d8b..acf0cebd 100644
--- a/src/guitos/infrastructure/localForageBudgetRepository.ts
+++ b/src/guitos/infrastructure/localForageBudgetRepository.ts
@@ -1,12 +1,21 @@
+import localforage from "localforage";
import { Budget } from "../domain/budget";
import type { BudgetRepository } from "../domain/budgetRepository";
import type { Uuid } from "../domain/uuid";
-import { budgetsDB } from "./localForageDb";
export class localForageBudgetRepository implements BudgetRepository {
+ private readonly budgetsDB;
+
+ constructor() {
+ this.budgetsDB = localforage.createInstance({
+ name: "guitos",
+ storeName: "budgets",
+ });
+ }
+
async get(id: Uuid): Promise {
try {
- const budget = await budgetsDB.getItem(id.toString());
+ const budget = await this.budgetsDB.getItem(id.toString());
if (!budget) throw new Error();
return budget;
} catch (e) {
@@ -17,9 +26,9 @@ export class localForageBudgetRepository implements BudgetRepository {
async getAll(): Promise {
try {
const list: Budget[] = [];
- for (const item of await budgetsDB.keys()) {
+ for (const item of await this.budgetsDB.keys()) {
if (item) {
- const budget = await budgetsDB.getItem(item);
+ const budget = await this.budgetsDB.getItem(item);
if (budget) {
list.push(budget);
}
@@ -33,7 +42,10 @@ export class localForageBudgetRepository implements BudgetRepository {
async update(id: Uuid, newBudget: Budget): Promise {
try {
- await budgetsDB.setItem(id.toString(), Budget.toSafeFormat(newBudget));
+ await this.budgetsDB.setItem(
+ id.toString(),
+ Budget.toSafeFormat(newBudget),
+ );
return true;
} catch {
return false;
@@ -42,7 +54,7 @@ export class localForageBudgetRepository implements BudgetRepository {
async delete(id: Uuid): Promise {
try {
- await budgetsDB.removeItem(id.toString());
+ await this.budgetsDB.removeItem(id.toString());
return true;
} catch {
return false;
diff --git a/src/guitos/infrastructure/localForageCalcHistRepository.ts b/src/guitos/infrastructure/localForageCalcHistRepository.ts
index cfc8a8e3..3efc62ac 100644
--- a/src/guitos/infrastructure/localForageCalcHistRepository.ts
+++ b/src/guitos/infrastructure/localForageCalcHistRepository.ts
@@ -1,11 +1,19 @@
+import localforage from "localforage";
import type { CalcHistRepository } from "../domain/calcHistRepository";
import type { CalculationHistoryItem } from "../domain/calculationHistoryItem";
-import { calcHistDB } from "./localForageDb";
export class localForageCalcHistRepository implements CalcHistRepository {
+ private readonly calcHistDB;
+
+ constructor() {
+ this.calcHistDB = localforage.createInstance({
+ name: "guitos",
+ storeName: "calcHistDB",
+ });
+ }
async get(id: string): Promise {
try {
- return await calcHistDB.getItem(id);
+ return await this.calcHistDB.getItem(id);
} catch {
return null;
}
@@ -14,10 +22,10 @@ export class localForageCalcHistRepository implements CalcHistRepository {
async getAll(): Promise {
try {
const list: CalculationHistoryItem[][] = [];
- for (const item of await calcHistDB.keys()) {
+ for (const item of await this.calcHistDB.keys()) {
if (item) {
const calcHist =
- await calcHistDB.getItem(item);
+ await this.calcHistDB.getItem(item);
if (calcHist) {
list.push(calcHist);
}
@@ -34,7 +42,7 @@ export class localForageCalcHistRepository implements CalcHistRepository {
newCalcHist: CalculationHistoryItem[],
): Promise {
try {
- await calcHistDB.setItem(
+ await this.calcHistDB.setItem(
id,
newCalcHist.map((item) => item),
);
@@ -46,7 +54,7 @@ export class localForageCalcHistRepository implements CalcHistRepository {
async delete(id: string): Promise {
try {
- await calcHistDB.removeItem(id);
+ await this.calcHistDB.removeItem(id);
return true;
} catch {
return false;
diff --git a/src/guitos/infrastructure/localForageDb.ts b/src/guitos/infrastructure/localForageDb.ts
deleted file mode 100644
index 394a4f5a..00000000
--- a/src/guitos/infrastructure/localForageDb.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import localforage from "localforage";
-
-export const budgetsDB = localforage.createInstance({
- name: "guitos",
- storeName: "budgets",
-});
-
-export const optionsDB = localforage.createInstance({
- name: "guitos",
- storeName: "options",
-});
-
-export const calcHistDB = localforage.createInstance({
- name: "guitos",
- storeName: "calcHistDB",
-});
diff --git a/src/guitos/sections/Budget/BudgetPage.test.tsx b/src/guitos/sections/Budget/BudgetPage.test.tsx
new file mode 100644
index 00000000..980a22ec
--- /dev/null
+++ b/src/guitos/sections/Budget/BudgetPage.test.tsx
@@ -0,0 +1,120 @@
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { act } from "react-dom/test-utils";
+import { BrowserRouter } from "react-router-dom";
+import { describe, expect, it } from "vitest";
+import { BudgetMother } from "../../domain/budget.mother";
+import {
+ budgetContextSpy,
+ redoMock,
+ setBudgetMock,
+ setNotificationsMock,
+ testBudgetContext,
+ undoMock,
+} from "../../../setupTests";
+import { BudgetPage } from "./BudgetPage";
+import { localForageBudgetRepository } from "../../infrastructure/localForageBudgetRepository";
+
+const budgetRepository = new localForageBudgetRepository();
+
+describe("BudgetPage", () => {
+ const comp = (
+
+
+
+ );
+
+ it("matches snapshot", () => {
+ render(comp);
+ expect(comp).toMatchSnapshot();
+ });
+
+ it("renders initial state", async () => {
+ render(comp);
+ const newButton = screen.getAllByRole("button", { name: "new budget" });
+ await act(async () => {
+ await userEvent.click(newButton[0]);
+ });
+ expect(screen.getByLabelText("delete budget")).toBeInTheDocument();
+ });
+
+ it("responds to new budget keyboard shortcut", async () => {
+ render(comp);
+ await userEvent.type(await screen.findByTestId("header"), "a");
+ expect(setBudgetMock).toHaveBeenCalled();
+ });
+
+ it("removes budget when clicking on delete budget button", async () => {
+ render(comp);
+ const deleteButton = await screen.findAllByRole("button", {
+ name: "delete budget",
+ });
+ await userEvent.click(deleteButton[0]);
+ await userEvent.click(
+ await screen.findByRole("button", { name: "confirm budget deletion" }),
+ );
+ await expect(
+ budgetRepository.get(BudgetMother.testBudget().id),
+ ).rejects.toThrow();
+ });
+
+ it.skip("clones budget when clicking on clone budget button", async () => {
+ render(comp);
+ const newButton = await screen.findAllByRole("button", {
+ name: "new budget",
+ });
+ await userEvent.click(newButton[0]);
+
+ const cloneButton = screen.getAllByRole("button", {
+ name: "clone budget",
+ });
+ await userEvent.click(cloneButton[0]);
+ expect(setBudgetMock).toHaveBeenCalledWith(
+ BudgetMother.testBudgetClone(),
+ true,
+ );
+ });
+
+ it.skip("responds to clone budget keyboard shortcut", async () => {
+ render(comp);
+ const newButton = await screen.findAllByRole("button", {
+ name: "new budget",
+ });
+ await userEvent.click(newButton[0]);
+
+ await userEvent.type(await screen.findByTestId("header"), "c");
+ expect(setBudgetMock).toHaveBeenCalledWith(
+ BudgetMother.testBudgetClone(),
+ true,
+ );
+ });
+
+ it("responds to undo change keyboard shortcut", async () => {
+ cleanup();
+ budgetContextSpy.mockReturnValue({ ...testBudgetContext, canUndo: true });
+ render(comp);
+ await userEvent.type(await screen.findByTestId("header"), "u");
+ expect(undoMock).toHaveBeenCalled();
+ });
+
+ it("responds to redo change keyboard shortcut", async () => {
+ cleanup();
+ budgetContextSpy.mockReturnValue({ ...testBudgetContext, canRedo: true });
+ render(comp);
+ await userEvent.type(await screen.findByTestId("header"), "r");
+ expect(redoMock).toHaveBeenCalled();
+ });
+
+ it("responds to clear notifications keyboard shortcut", async () => {
+ render(comp);
+ setNotificationsMock.mockClear();
+ await userEvent.type(await screen.findByTestId("header"), "{Escape}");
+ expect(setNotificationsMock).toHaveBeenCalledWith([]);
+ });
+
+ it("responds to show graphs keyboard shortcut", async () => {
+ render(comp);
+ await userEvent.type(await screen.findByTestId("header"), "i");
+ expect(screen.getByRole("status")).toBeInTheDocument();
+ });
+});