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(); + }); +});