refactor: separate react/react-native
This commit is contained in:
parent
748ac2476c
commit
f3156eee61
@ -1,7 +1,7 @@
|
||||
import { Redirect, Tabs } from "expo-router"
|
||||
import React from "react"
|
||||
|
||||
import { TabBarIcon } from "@/presentation/react/components/TabBarIcon"
|
||||
import { TabBarIcon } from "@/presentation/react-native/ui/TabBarIcon"
|
||||
import { useAuthentication } from "@/presentation/react/contexts/Authentication"
|
||||
|
||||
const TabLayout: React.FC = () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Redirect, useLocalSearchParams } from "expo-router"
|
||||
|
||||
import { HabitEditForm } from "@/presentation/react/components/HabitForm/HabitEditForm"
|
||||
import { HabitEditForm } from "@/presentation/react-native/components/HabitForm/HabitEditForm"
|
||||
import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
|
||||
|
||||
const HabitPage: React.FC = () => {
|
||||
|
@ -4,11 +4,11 @@ import { Agenda } from "react-native-calendars"
|
||||
import { Text } from "react-native-paper"
|
||||
import { SafeAreaView } from "react-native-safe-area-context"
|
||||
|
||||
import { getISODate, getNowDate } from "@/utils/dates"
|
||||
import { getISODate, getNowDateUTC } from "@/utils/dates"
|
||||
|
||||
const HistoryPage: React.FC = () => {
|
||||
const today = useMemo(() => {
|
||||
return getNowDate()
|
||||
return getNowDateUTC()
|
||||
}, [])
|
||||
const todayISO = getISODate(today)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { SafeAreaView } from "react-native-safe-area-context"
|
||||
import { ActivityIndicator, Button, Text } from "react-native-paper"
|
||||
|
||||
import { HabitsMainPage } from "@/presentation/react/components/HabitsMainPage/HabitsMainPage"
|
||||
import { HabitsMainPage } from "@/presentation/react-native/components/HabitsMainPage/HabitsMainPage"
|
||||
import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
|
||||
import { useAuthentication } from "@/presentation/react/contexts/Authentication"
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { HabitCreateForm } from "@/presentation/react/components/HabitForm/HabitCreateForm"
|
||||
import { HabitCreateForm } from "@/presentation/react-native/components/HabitForm/HabitCreateForm"
|
||||
import { useAuthentication } from "@/presentation/react/contexts/Authentication"
|
||||
|
||||
const NewHabitPage: React.FC = () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Redirect, Tabs } from "expo-router"
|
||||
import React from "react"
|
||||
|
||||
import { TabBarIcon } from "@/presentation/react/components/TabBarIcon"
|
||||
import { TabBarIcon } from "@/presentation/react-native/ui/TabBarIcon"
|
||||
import { useAuthentication } from "@/presentation/react/contexts/Authentication"
|
||||
|
||||
const TabLayout: React.FC = () => {
|
||||
|
@ -1,10 +1,18 @@
|
||||
import { createClient } from "@supabase/supabase-js"
|
||||
import {
|
||||
createClient,
|
||||
type User as SupabaseUserType,
|
||||
} from "@supabase/supabase-js"
|
||||
import { AppState, Platform } from "react-native"
|
||||
import "react-native-url-polyfill/auto"
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage"
|
||||
|
||||
import type { Database } from "./supabase-types"
|
||||
|
||||
export type SupabaseUser = SupabaseUserType
|
||||
export type SupabaseHabit = Database["public"]["Tables"]["habits"]["Row"]
|
||||
export type SupabaseHabitProgress =
|
||||
Database["public"]["Tables"]["habits_progresses"]["Row"]
|
||||
|
||||
const SUPABASE_URL =
|
||||
process.env["EXPO_PUBLIC_SUPABASE_URL"] ??
|
||||
"https://wjtwtzxreersqfvfgxrz.supabase.co"
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"preset": "jest-expo",
|
||||
"roots": ["./"],
|
||||
"setupFilesAfterEnv": ["@testing-library/react-native/extend-expect"],
|
||||
"setupFilesAfterEnv": ["<rootDir>/tests/setup.ts"],
|
||||
"fakeTimers": {
|
||||
"enableGlobally": true
|
||||
},
|
||||
@ -10,7 +10,7 @@
|
||||
"coverageReporters": ["text", "text-summary", "cobertura"],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/**/*.{ts,tsx}",
|
||||
"!<rootDir>/presentation/react/components/ExternalLink.tsx",
|
||||
"!<rootDir>/presentation/react-native/ui/ExternalLink.tsx",
|
||||
"!<rootDir>/.expo",
|
||||
"!<rootDir>/app/+html.tsx",
|
||||
"!<rootDir>/app/**/_layout.tsx",
|
||||
|
@ -24,8 +24,8 @@ import { GOAL_FREQUENCIES, GOAL_TYPES } from "@/domain/entities/Goal"
|
||||
import type { HabitCreateData } from "@/domain/entities/Habit"
|
||||
import { HabitCreateSchema } from "@/domain/entities/Habit"
|
||||
import type { User } from "@/domain/entities/User"
|
||||
import { useHabitsTracker } from "../../contexts/HabitsTracker"
|
||||
import { useBoolean } from "../../hooks/useBoolean"
|
||||
import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
|
||||
import { useBoolean } from "@/presentation/react/hooks/useBoolean"
|
||||
import { IconSelectorModal } from "./IconSelectorModal"
|
||||
|
||||
export interface HabitCreateFormProps {
|
||||
@ -37,11 +37,10 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isValid },
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
|
||||
formState: { errors, isValid },
|
||||
} = useForm<HabitCreateData>({
|
||||
mode: "onChange",
|
||||
resolver: zodResolver(HabitCreateSchema),
|
@ -20,8 +20,8 @@ import ColorPicker, {
|
||||
|
||||
import type { Habit, HabitEditData } from "@/domain/entities/Habit"
|
||||
import { HabitEditSchema } from "@/domain/entities/Habit"
|
||||
import { useHabitsTracker } from "../../contexts/HabitsTracker"
|
||||
import { useBoolean } from "../../hooks/useBoolean"
|
||||
import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
|
||||
import { useBoolean } from "@/presentation/react/hooks/useBoolean"
|
||||
import { IconSelectorModal } from "./IconSelectorModal"
|
||||
|
||||
export interface HabitEditFormProps {
|
||||
@ -33,8 +33,8 @@ export const HabitEditForm: React.FC<HabitEditFormProps> = ({ habit }) => {
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors, isValid },
|
||||
handleSubmit,
|
||||
} = useForm<HabitEditData>({
|
||||
mode: "onChange",
|
||||
resolver: zodResolver(HabitEditSchema),
|
@ -3,7 +3,7 @@ import { Agenda } from "react-native-calendars"
|
||||
|
||||
import { GOAL_FREQUENCIES } from "@/domain/entities/Goal"
|
||||
import type { HabitsTracker } from "@/domain/entities/HabitsTracker"
|
||||
import { getISODate, getNowDate } from "@/utils/dates"
|
||||
import { getISODate, getNowDateUTC } from "@/utils/dates"
|
||||
import { HabitsEmpty } from "./HabitsEmpty"
|
||||
import { HabitsList } from "./HabitsList"
|
||||
|
||||
@ -14,7 +14,7 @@ export interface HabitsMainPageProps {
|
||||
export const HabitsMainPage: React.FC<HabitsMainPageProps> = (props) => {
|
||||
const { habitsTracker } = props
|
||||
|
||||
const today = getNowDate()
|
||||
const today = getNowDateUTC()
|
||||
const todayISO = getISODate(today)
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(today)
|
@ -1,6 +1,6 @@
|
||||
import renderer from "react-test-renderer"
|
||||
|
||||
import { ExternalLink } from "@/presentation/react/components/ExternalLink"
|
||||
import { ExternalLink } from "@/presentation/react-native/ui/ExternalLink"
|
||||
|
||||
describe("<ExternalLink />", () => {
|
||||
it("renders correctly", () => {
|
@ -1,6 +1,6 @@
|
||||
import renderer from "react-test-renderer"
|
||||
|
||||
import { TabBarIcon } from "@/presentation/react/components/TabBarIcon"
|
||||
import { TabBarIcon } from "@/presentation/react-native/ui/TabBarIcon"
|
||||
|
||||
describe("<TabBarIcon />", () => {
|
||||
it("renders correctly", () => {
|
@ -2,8 +2,8 @@ import { act, renderHook } from "@testing-library/react-native"
|
||||
|
||||
import { useBoolean } from "@/presentation/react/hooks/useBoolean"
|
||||
|
||||
describe("hooks/useBoolean", () => {
|
||||
beforeEach(() => {
|
||||
describe("presentation/react/hooks/useBoolean", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
@ -11,51 +11,76 @@ describe("hooks/useBoolean", () => {
|
||||
|
||||
for (const initialValue of initialValues) {
|
||||
it(`should set the initial value to ${initialValue}`, () => {
|
||||
// Arrange - Given
|
||||
const { result } = renderHook(() => {
|
||||
return useBoolean({ initialValue })
|
||||
})
|
||||
|
||||
// Assert - Then
|
||||
expect(result.current.value).toBe(initialValue)
|
||||
})
|
||||
}
|
||||
|
||||
it("should by default set the initial value to false", () => {
|
||||
// Arrange - Given
|
||||
const { result } = renderHook(() => {
|
||||
return useBoolean()
|
||||
})
|
||||
|
||||
// Assert - Then
|
||||
expect(result.current.value).toBe(false)
|
||||
})
|
||||
|
||||
it("should toggle the value", async () => {
|
||||
// Arrange - Given
|
||||
const { result } = renderHook(() => {
|
||||
return useBoolean({ initialValue: false })
|
||||
})
|
||||
|
||||
// Act - When
|
||||
await act(() => {
|
||||
return result.current.toggle()
|
||||
})
|
||||
|
||||
// Assert - Then
|
||||
expect(result.current.value).toBe(true)
|
||||
|
||||
// Act - When
|
||||
await act(() => {
|
||||
return result.current.toggle()
|
||||
})
|
||||
|
||||
// Assert - Then
|
||||
expect(result.current.value).toBe(false)
|
||||
})
|
||||
|
||||
it("should set the value to true", async () => {
|
||||
// Arrange - Given
|
||||
const { result } = renderHook(() => {
|
||||
return useBoolean({ initialValue: false })
|
||||
})
|
||||
|
||||
// Act - When
|
||||
await act(() => {
|
||||
return result.current.setTrue()
|
||||
})
|
||||
|
||||
// Assert - Then
|
||||
expect(result.current.value).toBe(true)
|
||||
})
|
||||
|
||||
it("should set the value to false", async () => {
|
||||
// Arrange - Given
|
||||
const { result } = renderHook(() => {
|
||||
return useBoolean({ initialValue: true })
|
||||
})
|
||||
|
||||
// Act - When
|
||||
await act(() => {
|
||||
return result.current.setFalse()
|
||||
})
|
||||
|
||||
// Assert - Then
|
||||
expect(result.current.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
75
presentation/react/hooks/__tests__/usePresenterState.test.ts
Normal file
75
presentation/react/hooks/__tests__/usePresenterState.test.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { act, renderHook } from "@testing-library/react-native"
|
||||
|
||||
import { usePresenterState } from "@/presentation/react/hooks/usePresenterState"
|
||||
import { Presenter } from "@/presentation/presenters/_Presenter"
|
||||
|
||||
interface MockCountPresenterState {
|
||||
count: number
|
||||
}
|
||||
|
||||
class MockCountPresenter extends Presenter<MockCountPresenterState> {
|
||||
public constructor(initialState: MockCountPresenterState) {
|
||||
super(initialState)
|
||||
}
|
||||
|
||||
public increment(): void {
|
||||
this.setState((state) => {
|
||||
state.count = state.count + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe("presentation/react/hooks/usePresenterState", () => {
|
||||
it("should return the initial state from the presenter", async () => {
|
||||
// Arrange - Given
|
||||
const initialState = { count: 0 }
|
||||
const presenter = new MockCountPresenter(initialState)
|
||||
|
||||
// Act - When
|
||||
const { result } = renderHook(() => {
|
||||
return usePresenterState(presenter)
|
||||
})
|
||||
|
||||
// Assert - Then
|
||||
expect(result.current).toEqual(initialState)
|
||||
})
|
||||
|
||||
it("should update state when presenter state changes", async () => {
|
||||
// Arrange - Given
|
||||
const initialState = { count: 0 }
|
||||
const presenter = new MockCountPresenter(initialState)
|
||||
const subscribe = jest.spyOn(presenter, "subscribe")
|
||||
const { result } = renderHook(() => {
|
||||
return usePresenterState(presenter)
|
||||
})
|
||||
|
||||
// Act - When
|
||||
await act(() => {
|
||||
presenter.increment()
|
||||
})
|
||||
|
||||
// Assert - Then
|
||||
expect(result.current.count).toBe(1)
|
||||
expect(subscribe).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should unsubscribe from presenter on unmount", async () => {
|
||||
// Arrange - Given
|
||||
const initialState = { count: 0 }
|
||||
const presenter = new MockCountPresenter(initialState)
|
||||
const unsubscribe = jest.spyOn(presenter, "unsubscribe")
|
||||
const { result, unmount } = renderHook(() => {
|
||||
return usePresenterState(presenter)
|
||||
})
|
||||
|
||||
// Act - When
|
||||
unmount()
|
||||
await act(() => {
|
||||
presenter.increment()
|
||||
})
|
||||
|
||||
// Assert - Then
|
||||
expect(result.current.count).toBe(0)
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
@ -2,9 +2,10 @@ import { useState } from "react"
|
||||
|
||||
export interface UseBooleanResult {
|
||||
value: boolean
|
||||
toggle: () => void
|
||||
setValue: React.Dispatch<React.SetStateAction<boolean>>
|
||||
setTrue: () => void
|
||||
setFalse: () => void
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export interface UseBooleanOptions {
|
||||
@ -43,6 +44,7 @@ export const useBoolean = (
|
||||
|
||||
return {
|
||||
value,
|
||||
setValue,
|
||||
toggle,
|
||||
setTrue,
|
||||
setFalse,
|
||||
|
1
tests/setup.ts
Normal file
1
tests/setup.ts
Normal file
@ -0,0 +1 @@
|
||||
import "@testing-library/react-native/extend-expect"
|
@ -1,6 +1,12 @@
|
||||
import { getISODate, getWeekNumber } from "../dates"
|
||||
import { getISODate, getNowDateUTC, getWeekNumber } from "../dates"
|
||||
|
||||
describe("utils/dates", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.resetAllMocks()
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
describe("getISODate", () => {
|
||||
it("should return the correct date in ISO format (e.g: 2012-05-23)", () => {
|
||||
// Arrange - Given
|
||||
@ -15,6 +21,25 @@ describe("utils/dates", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("getNowDateUTC", () => {
|
||||
it("should return the current UTC date", () => {
|
||||
// Arrange - Given
|
||||
const mockDate = new Date("2024-05-01T12:00:00Z")
|
||||
jest.useFakeTimers({ now: mockDate })
|
||||
Date.UTC = jest.fn(() => {
|
||||
return mockDate.getTime()
|
||||
})
|
||||
|
||||
// Act - When
|
||||
const result = getNowDateUTC()
|
||||
|
||||
// Assert - Then
|
||||
const expected = new Date("2024-05-01T12:00:00.000Z")
|
||||
expect(result).toEqual(expected)
|
||||
expect(Date.UTC).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getWeekNumber", () => {
|
||||
it("should return the correct week number for a given date (e.g: 2020-01-01)", () => {
|
||||
// Arrange - Given
|
||||
|
39
utils/__tests__/zod.test.ts
Normal file
39
utils/__tests__/zod.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import type { ZodIssue } from "zod"
|
||||
import { ZodError } from "zod"
|
||||
|
||||
import { getErrorsFieldsFromZodError } from "../zod"
|
||||
|
||||
const zodIssue: ZodIssue = {
|
||||
code: "too_small",
|
||||
minimum: 1,
|
||||
type: "string",
|
||||
inclusive: true,
|
||||
exact: false,
|
||||
message: "String must contain at least 1 character(s)",
|
||||
path: ["name"],
|
||||
}
|
||||
|
||||
describe("utils/zod", () => {
|
||||
describe("getErrorsFieldsFromZodError", () => {
|
||||
it("should return an array of the fields that have errors", () => {
|
||||
// Arrange - Given
|
||||
const error = new ZodError([
|
||||
{
|
||||
...zodIssue,
|
||||
path: ["field1"],
|
||||
},
|
||||
{
|
||||
...zodIssue,
|
||||
path: ["field2"],
|
||||
},
|
||||
])
|
||||
|
||||
// Act - When
|
||||
const result = getErrorsFieldsFromZodError(error)
|
||||
|
||||
// Assert - Then
|
||||
const expected = ["field1", "field2"]
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
@ -11,7 +11,7 @@ export const getISODate = (date: Date): string => {
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
export const getNowDate = (): Date => {
|
||||
export const getNowDateUTC = (): Date => {
|
||||
const date = new Date()
|
||||
const milliseconds = Date.UTC(
|
||||
date.getFullYear(),
|
||||
|
@ -3,5 +3,8 @@ import type { ZodError } from "zod"
|
||||
export const getErrorsFieldsFromZodError = <T>(
|
||||
error: ZodError<T>,
|
||||
): Array<keyof T> => {
|
||||
return Object.keys(error.format()) as Array<keyof T>
|
||||
const fields = Object.keys(error.format()) as Array<keyof T>
|
||||
return fields.filter((field) => {
|
||||
return field !== "_errors"
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user