diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index f4b3ea7..10e4064 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -11,4 +11,4 @@ issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: gabriellogan -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] \ No newline at end of file +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 4d04ff0..cd2f261 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -19,7 +19,7 @@ jobs: with: node-version: 20 cache: "npm" - + - name: Install dependencies run: yarn install @@ -28,6 +28,6 @@ jobs: - name: Run lint run: yarn lint - + - name: Run test - run: yarn test:verbose:coverage \ No newline at end of file + run: yarn test:verbose:coverage diff --git a/__mocks__/svgMock.js b/__mocks__/svgMock.js new file mode 100644 index 0000000..a30853a --- /dev/null +++ b/__mocks__/svgMock.js @@ -0,0 +1 @@ +module.exports = "SvgMock"; diff --git a/jest.config.js b/jest.config.js index b03834e..e5dd38a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,4 +3,7 @@ module.exports = { preset: "react-native", setupFilesAfterEnv: ["./jest/setupFilesAfterEnv.ts"], setupFiles: ["./jest/setupFiles.js"], + moduleNameMapper: { + "\\.svg": "/__mocks__/svgMock.js", + }, }; diff --git a/jest/setupFiles.js b/jest/setupFiles.js index 61e4eab..0099594 100644 --- a/jest/setupFiles.js +++ b/jest/setupFiles.js @@ -31,3 +31,32 @@ jest.mock("@react-navigation/native", () => { jest.mock("@react-native-async-storage/async-storage", () => require("@react-native-async-storage/async-storage/jest/async-storage-mock") ); + +jest.mock("react-i18next", () => ({ + // this mock makes sure any components using the translate hook can use it without a warning being shown + useTranslation: () => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}), + }, + }; + }, + initReactI18next: { + type: "3rdParty", + init: () => {}, + }, +})); + +jest.mock("react-native-responsive-fontsize", () => ({ + RFValue: jest.fn(), + RFPercentage: jest.fn(), + fixedRFValue: jest.fn(), +})); + +jest.mock("react-native-responsive-screen", () => ({ + widthPercentageToDP: jest.fn(), + heightPercentageToDP: jest.fn(), + width: jest.fn(), + height: jest.fn(), +})); diff --git a/src/components/DrawerContent/__tests__/DrawerContent.test.tsx b/src/components/DrawerContent/__tests__/DrawerContent.test.tsx new file mode 100644 index 0000000..29caa2f --- /dev/null +++ b/src/components/DrawerContent/__tests__/DrawerContent.test.tsx @@ -0,0 +1,9 @@ +import { render } from "@testing-library/react-native"; + +import CustomDrawerContent from ".."; + +describe("DrawerContent", () => { + it("should render correctly", () => { + render(); + }); +}); diff --git a/src/components/DrawerContent/__tests__/drawerMenu.test.ts b/src/components/DrawerContent/__tests__/drawerMenu.test.ts new file mode 100644 index 0000000..8cd7c42 --- /dev/null +++ b/src/components/DrawerContent/__tests__/drawerMenu.test.ts @@ -0,0 +1,24 @@ +import drawerMenu from "../drawerMenu"; + +describe("drawerMenu", () => { + it("should have the correct number of items", () => { + expect(drawerMenu).toHaveLength(7); + }); + + it("should have the correct structure for each item", () => { + drawerMenu.forEach((item) => { + expect(item).toHaveProperty("title"); + if ("route" in item) { + expect(item).toHaveProperty("route"); + expect(item).not.toHaveProperty("menuList"); + } else { + expect(item).toHaveProperty("menuList"); + expect(item).not.toHaveProperty("route"); + item.menuList.forEach((subItem) => { + expect(subItem).toHaveProperty("title"); + expect(subItem).toHaveProperty("route"); + }); + } + }); + }); +}); diff --git a/src/components/ThemeContext/__tests__/ThemeContext.test.tsx b/src/components/ThemeContext/__tests__/ThemeContext.test.tsx new file mode 100644 index 0000000..f2445be --- /dev/null +++ b/src/components/ThemeContext/__tests__/ThemeContext.test.tsx @@ -0,0 +1,132 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { + render, + fireEvent, + screen, + waitFor, +} from "@testing-library/react-native"; +import { Text, TouchableOpacity } from "react-native"; + +import ThemeProvider, { useTheme } from ".."; + +describe("ThemeContext", () => { + describe("ThemeProvider", () => { + test("renders children correctly", () => { + render( + + Test + + ); + + expect(screen.getByText("Test")).toBeDefined(); + }); + + test("toggles theme correctly", () => { + const TestComponent = () => { + const { theme, toggleTheme } = useTheme(); + + return ( + toggleTheme(theme === "light" ? "dark" : "light")} + > + {theme} + + ); + }; + + render( + + + + ); + + const toggleButton = screen.getByText("light"); + + fireEvent.press(toggleButton); + + expect(screen.getByText("dark")).toBeDefined(); + }); + + describe("useEffect", () => { + test("themeSelected from AsyncStorage is system", async () => { + const valueStoraged = "system"; + + (AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce( + valueStoraged + ); + + render( + + Test + + ); + + await waitFor(() => { + expect(screen.getByText("Test")).toBeDefined(); + }); + }); + + test("themeSelected from AsyncStorage is not system", async () => { + const valueStoraged = "dark"; + + (AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce( + valueStoraged + ); + + render( + + Test + + ); + + await waitFor(() => { + expect(screen.getByText("Test")).toBeDefined(); + }); + }); + + test("error on AsyncStorage.getItem", async () => { + (AsyncStorage.getItem as jest.Mock).mockRejectedValueOnce( + new Error("AsyncStorage error") + ); + + render( + + Test + + ); + + await waitFor(() => { + expect(screen.getByText("Test")).toBeDefined(); + }); + }); + }); + }); + + describe("useTheme", () => { + test("returns theme and toggleTheme function", () => { + const TestComponent = () => { + const { theme, toggleTheme } = useTheme(); + + return ( + toggleTheme(theme === "light" ? "dark" : "light")} + > + {theme} + + ); + }; + + render( + + + + ); + + const toggleButton = screen.getByText("light"); + + fireEvent.press(toggleButton); + + expect(screen.getByText("dark")).toBeDefined(); + }); + }); +}); diff --git a/src/components/ThemeContext/index.tsx b/src/components/ThemeContext/index.tsx index e348ec2..2fca66c 100644 --- a/src/components/ThemeContext/index.tsx +++ b/src/components/ThemeContext/index.tsx @@ -19,13 +19,7 @@ export default function ThemeProvider({ children }: { children: ReactNode }) { ); const toggleTheme = async (newTheme: Theme) => { - try { - setTheme(newTheme); - } catch { - Alert.alert( - "Something strange happened when trying to change the theme, contact the developer" - ); - } + setTheme(newTheme); }; const contextValue: ThemeContextProps = { @@ -70,8 +64,6 @@ export default function ThemeProvider({ children }: { children: ReactNode }) { export const useTheme = () => { const context = useContext(ThemeContext) as ThemeContextProps; - if (!context) { - throw new Error("useTheme must be used within a ThemeProvider"); - } + return context; };