refactor: mocks data for tests

This commit is contained in:
Théo LUDWIG 2024-05-02 23:48:47 +02:00
parent f3156eee61
commit 06ef8515cb
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
29 changed files with 875 additions and 214 deletions

View File

@ -1,8 +1,8 @@
import { getISODate, getWeekNumber } from "@/utils/dates"
import type { Habit } from "./Habit"
import type { HabitProgress } from "./HabitProgress"
import type { GoalProgress } from "./Goal"
import { GoalBooleanProgress, GoalNumericProgress } from "./Goal"
import type { Habit } from "./Habit"
import type { HabitProgress } from "./HabitProgress"
export interface HabitHistoryJSON {
habit: Habit

View File

@ -0,0 +1,106 @@
import { HABIT_MOCK } from "@/tests/mocks/domain/Habit"
import { GOAL_FREQUENCIES } from "../Goal"
import { HabitsTracker } from "../HabitsTracker"
import { HabitHistory } from "../HabitHistory"
import { HABIT_PROGRESS_MOCK } from "@/tests/mocks/domain/HabitProgress"
describe("domain/entities/HabitsTracker", () => {
describe("HabitsTracker.default", () => {
for (const frequency of GOAL_FREQUENCIES) {
it(`should return empty habitsHistory for ${frequency}`, () => {
const habitsTracker = HabitsTracker.default()
expect(habitsTracker.habitsHistory[frequency]).toEqual([])
})
}
})
describe("getAllHabitsHistory", () => {
it("should return all habits history", () => {
const habitsTracker = HabitsTracker.default()
const habit = HABIT_MOCK.examplesByNames.Walk
habitsTracker.addHabit(habit)
expect(habitsTracker.getAllHabitsHistory()).toEqual([
new HabitHistory({
habit,
progressHistory: [],
}),
])
})
it("should return empty array when no habits are added", () => {
const habitsTracker = HabitsTracker.default()
expect(habitsTracker.getAllHabitsHistory()).toEqual([])
})
})
describe("getHabitHistoryById", () => {
it("should return habit history by id", () => {
const habitsTracker = HabitsTracker.default()
const habit = HABIT_MOCK.examplesByNames.Walk
habitsTracker.addHabit(habit)
expect(habitsTracker.getHabitHistoryById(habit.id)).toEqual(
new HabitHistory({
habit,
progressHistory: [],
}),
)
})
it("should return undefined when habit is not found", () => {
const habitsTracker = HabitsTracker.default()
expect(habitsTracker.getHabitHistoryById("invalid-id")).toBeUndefined()
})
})
describe("addHabit", () => {
it("should add habit to habitsHistory", () => {
const habitsTracker = HabitsTracker.default()
const habit = HABIT_MOCK.examplesByNames.Walk
habitsTracker.addHabit(habit)
expect(habitsTracker.habitsHistory[habit.goal.frequency]).toEqual([
new HabitHistory({
habit,
progressHistory: [],
}),
])
})
})
describe("editHabit", () => {
it("should edit habit in habitsHistory", () => {
const habitsTracker = HabitsTracker.default()
const habit = HABIT_MOCK.examplesByNames.Walk
habitsTracker.addHabit(habit)
habit.name = "Run"
habitsTracker.editHabit(habit)
expect(habitsTracker.habitsHistory[habit.goal.frequency]).toEqual([
new HabitHistory({
habit,
progressHistory: [],
}),
])
})
it("should not edit habit in habitsHistory when habit is not found", () => {
const habitsTracker = HabitsTracker.default()
const habit = HABIT_MOCK.examplesByNames.Walk
habitsTracker.editHabit(habit)
expect(habitsTracker.habitsHistory[habit.goal.frequency]).toEqual([])
})
})
describe("updateHabitProgress", () => {
it("should update habit progress in habitsHistory (add new habit progress if not yet added)", () => {
const habitsTracker = HabitsTracker.default()
const habit = HABIT_MOCK.examplesByNames["Clean the house"]
habitsTracker.addHabit(habit)
habitsTracker.updateHabitProgress(HABIT_PROGRESS_MOCK.exampleByIds[1])
expect(habitsTracker.habitsHistory[habit.goal.frequency]).toEqual([
new HabitHistory({
habit,
progressHistory: [HABIT_PROGRESS_MOCK.exampleByIds[1]],
}),
])
})
})
})

View File

@ -1,19 +1,19 @@
import { AuthenticationUseCase } from "@/domain/use-cases/Authentication"
import { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate"
import { HabitEditUseCase } from "@/domain/use-cases/HabitEdit"
import { HabitGoalProgressUpdateUseCase } from "@/domain/use-cases/HabitGoalProgressUpdate"
import { HabitStopUseCase } from "@/domain/use-cases/HabitStop"
import { AuthenticationPresenter } from "@/presentation/presenters/Authentication"
import { RetrieveHabitsTrackerUseCase } from "../domain/use-cases/RetrieveHabitsTracker"
import { HabitsTrackerPresenter } from "../presentation/presenters/HabitsTracker"
import { AuthenticationSupabaseRepository } from "./supabase/repositories/Authentication"
import { GetHabitProgressHistorySupabaseRepository } from "./supabase/repositories/GetHabitProgressHistory"
import { GetHabitsByUserIdSupabaseRepository } from "./supabase/repositories/GetHabitsByUserId"
import { supabaseClient } from "./supabase/supabase"
import { AuthenticationPresenter } from "@/presentation/presenters/Authentication"
import { HabitCreateSupabaseRepository } from "./supabase/repositories/HabitCreate"
import { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate"
import { HabitEditSupabaseRepository } from "./supabase/repositories/HabitEdit"
import { HabitEditUseCase } from "@/domain/use-cases/HabitEdit"
import { HabitProgressCreateSupabaseRepository } from "./supabase/repositories/HabitProgressCreate"
import { HabitProgressUpdateSupabaseRepository } from "./supabase/repositories/HabitProgressUpdate"
import { HabitGoalProgressUpdateUseCase } from "@/domain/use-cases/HabitGoalProgressUpdate"
import { HabitStopUseCase } from "@/domain/use-cases/HabitStop"
import { supabaseClient } from "./supabase/supabase"
/**
* Repositories

View File

@ -0,0 +1,79 @@
import type { Goal } from "@/domain/entities/Goal"
import { GoalBoolean, GoalNumeric } from "@/domain/entities/Goal"
import type { HabitCreateData, HabitEditData } from "@/domain/entities/Habit"
import { Habit } from "@/domain/entities/Habit"
import type {
SupabaseHabit,
SupabaseHabitInsert,
SupabaseHabitUpdate,
} from "../supabase"
export const habitSupabaseDTO = {
fromSupabaseToDomain: (supabaseHabit: SupabaseHabit): Habit => {
let goal: Goal
if (
supabaseHabit.goal_target != null &&
supabaseHabit.goal_target_unit != null
) {
goal = new GoalNumeric({
frequency: supabaseHabit.goal_frequency,
target: {
value: supabaseHabit.goal_target,
unit: supabaseHabit.goal_target_unit,
},
})
} else {
goal = new GoalBoolean({
frequency: supabaseHabit.goal_frequency,
})
}
const habit = new Habit({
id: supabaseHabit.id.toString(),
name: supabaseHabit.name,
color: supabaseHabit.color,
icon: supabaseHabit.icon,
userId: supabaseHabit.user_id.toString(),
startDate: new Date(supabaseHabit.start_date),
endDate:
supabaseHabit.end_date != null
? new Date(supabaseHabit.end_date)
: undefined,
goal,
})
return habit
},
fromDomainCreateDataToSupabaseInsert: (
habitCreateData: HabitCreateData,
): SupabaseHabitInsert => {
return {
name: habitCreateData.name,
color: habitCreateData.color,
icon: habitCreateData.icon,
goal_frequency: habitCreateData.goal.frequency,
...(habitCreateData.goal.target.type === "numeric"
? {
goal_target: habitCreateData.goal.target.value,
goal_target_unit: habitCreateData.goal.target.unit,
}
: {}),
}
},
fromDomainEditDataToSupabaseUpdate: (
habitEditData: HabitEditData,
): SupabaseHabitUpdate => {
return {
name: habitEditData.name,
color: habitEditData.color,
icon: habitEditData.icon,
end_date: habitEditData?.endDate?.toISOString(),
}
},
}
export const habitsSupabaseDTO = {
fromSupabaseToDomain: (supabaseHabits: SupabaseHabit[]): Habit[] => {
return supabaseHabits.map((supabaseHabit) => {
return habitSupabaseDTO.fromSupabaseToDomain(supabaseHabit)
})
},
}

View File

@ -0,0 +1,78 @@
import type { Goal, GoalProgress } from "@/domain/entities/Goal"
import {
GoalBooleanProgress,
GoalNumericProgress,
} from "@/domain/entities/Goal"
import { HabitProgress } from "@/domain/entities/HabitProgress"
import type { HabitProgressCreateOptions } from "@/domain/repositories/HabitProgressCreate"
import type { HabitProgressUpdateOptions } from "@/domain/repositories/HabitProgressUpdate"
import type {
SupabaseHabitProgress,
SupabaseHabitProgressInsert,
SupabaseHabitProgressUpdate,
} from "../supabase"
export const habitProgressSupabaseDTO = {
fromSupabaseToDomain: (
supabaseHabitProgress: SupabaseHabitProgress,
goal: Goal,
): HabitProgress => {
let goalProgress: GoalProgress | null = null
if (goal.isNumeric()) {
goalProgress = new GoalNumericProgress({
goal,
progress: supabaseHabitProgress.goal_progress,
})
} else if (goal.isBoolean()) {
goalProgress = new GoalBooleanProgress({
goal,
progress: supabaseHabitProgress.goal_progress === 1,
})
}
const habitProgress = new HabitProgress({
id: supabaseHabitProgress.id.toString(),
habitId: supabaseHabitProgress.habit_id.toString(),
goalProgress: goalProgress as GoalProgress,
date: new Date(supabaseHabitProgress.date),
})
return habitProgress
},
fromDomainDataToSupabaseInsert: (
habitProgressData: HabitProgressCreateOptions["habitProgressData"],
): SupabaseHabitProgressInsert => {
const { goalProgress, date, habitId } = habitProgressData
let goalProgressValue = goalProgress.isCompleted() ? 1 : 0
if (goalProgress.isNumeric()) {
goalProgressValue = goalProgress.progress
}
return {
habit_id: Number.parseInt(habitId, 10),
date: date.toISOString(),
goal_progress: goalProgressValue,
}
},
fromDomainDataToSupabaseUpdate: (
habitProgressData: HabitProgressUpdateOptions["habitProgressData"],
): SupabaseHabitProgressUpdate => {
const { goalProgress, date } = habitProgressData
let goalProgressValue = goalProgress.isCompleted() ? 1 : 0
if (goalProgress.isNumeric()) {
goalProgressValue = goalProgress.progress
}
return {
date: date.toISOString(),
goal_progress: goalProgressValue,
}
},
}
export const habitProgressHistorySupabaseDTO = {
fromSupabaseToDomain: (
supabaseHabitHistory: SupabaseHabitProgress[],
goal: Goal,
): HabitProgress[] => {
return supabaseHabitHistory.map((item) => {
return habitProgressSupabaseDTO.fromSupabaseToDomain(item, goal)
})
},
}

View File

@ -0,0 +1,100 @@
import type { GoalCreateData } from "@/domain/entities/Goal"
import { HABIT_MOCK } from "@/tests/mocks/domain/Habit"
import { SUPABASE_HABIT_MOCK } from "@/tests/mocks/supabase/Habit"
import { habitSupabaseDTO, habitsSupabaseDTO } from "../HabitDTO"
describe("infrastructure/supabase/data-transfer-objects/HabitDTO", () => {
describe("habitSupabaseDTO.fromSupabaseToDomain", () => {
for (const example of SUPABASE_HABIT_MOCK.examples) {
it(`should return correct Habit entity - ${example.name}`, () => {
expect(habitSupabaseDTO.fromSupabaseToDomain(example)).toEqual(
HABIT_MOCK.examplesByNames[
example.name as keyof typeof HABIT_MOCK.examplesByNames
],
)
})
}
})
describe("habitSupabaseDTO.fromDomainCreateDataToSupabaseInsert", () => {
for (const example of HABIT_MOCK.examples) {
it(`should return correct SupabaseHabitInsert entity - ${example.name}`, () => {
let goalData = {} as GoalCreateData
if (example.goal.isBoolean()) {
goalData = {
frequency: example.goal.frequency,
target: { type: "boolean" },
}
}
if (example.goal.isNumeric()) {
goalData = {
frequency: example.goal.frequency,
target: {
type: "numeric",
value: example.goal.target.value,
unit: example.goal.target.unit,
},
}
}
const supabaseData =
SUPABASE_HABIT_MOCK.examplesByNames[
example.name as keyof typeof SUPABASE_HABIT_MOCK.examplesByNames
]
expect(
habitSupabaseDTO.fromDomainCreateDataToSupabaseInsert({
userId: example.userId,
name: example.name,
color: example.color,
icon: example.icon,
goal: goalData,
}),
).toEqual({
name: supabaseData.name,
color: supabaseData.color,
icon: supabaseData.icon,
goal_frequency: supabaseData.goal_frequency,
...(supabaseData.goal_target != null &&
supabaseData.goal_target_unit != null
? {
goal_target: supabaseData.goal_target,
goal_target_unit: supabaseData.goal_target_unit,
}
: {}),
})
})
}
})
describe("habitSupabaseDTO.fromDomainEditDataToSupabaseUpdate", () => {
for (const example of HABIT_MOCK.examples) {
it(`should return correct SupabaseHabitUpdate entity - ${example.name}`, () => {
const supabaseData =
SUPABASE_HABIT_MOCK.examplesByNames[
example.name as keyof typeof SUPABASE_HABIT_MOCK.examplesByNames
]
expect(
habitSupabaseDTO.fromDomainEditDataToSupabaseUpdate({
name: example.name,
color: example.color,
icon: example.icon,
id: example.id,
userId: example.userId,
}),
).toEqual({
name: supabaseData.name,
color: supabaseData.color,
icon: supabaseData.icon,
})
})
}
})
describe("habitsSupabaseDTO.fromSupabaseToDomain", () => {
it("should return correct Habits entities", () => {
expect(
habitsSupabaseDTO.fromSupabaseToDomain(SUPABASE_HABIT_MOCK.examples),
).toEqual(HABIT_MOCK.examples)
})
})
})

View File

@ -0,0 +1,22 @@
import type { Habit } from "@/domain/entities/Habit"
import { HABIT_MOCK } from "@/tests/mocks/domain/Habit"
import { HABIT_PROGRESS_MOCK } from "@/tests/mocks/domain/HabitProgress"
import { SUPABASE_HABIT_PROGRESS_MOCK } from "@/tests/mocks/supabase/HabitProgress"
import { habitProgressSupabaseDTO } from "../HabitProgressDTO"
describe("infrastructure/supabase/data-transfer-objects/HabitProgressDTO", () => {
describe("habitProgressSupabaseDTO.fromSupabaseToDomain", () => {
for (const example of SUPABASE_HABIT_PROGRESS_MOCK.examples) {
it(`should return correct HabitProgress entity - ${example.id}`, () => {
const habit = HABIT_MOCK.examplesByIds[example.habit_id] as Habit
expect(
habitProgressSupabaseDTO.fromSupabaseToDomain(example, habit.goal),
).toEqual(
HABIT_PROGRESS_MOCK.exampleByIds[
example.id as keyof typeof HABIT_PROGRESS_MOCK.exampleByIds
],
)
})
}
})
})

View File

@ -1,8 +1,8 @@
import type { Session } from "@supabase/supabase-js"
import type { AuthenticationRepository } from "@/domain/repositories/Authentication"
import { SupabaseRepository } from "./_SupabaseRepository"
import { User } from "@/domain/entities/User"
import type { AuthenticationRepository } from "@/domain/repositories/Authentication"
import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository"
export class AuthenticationSupabaseRepository
extends SupabaseRepository

View File

@ -1,11 +1,6 @@
import type { GetHabitProgressHistoryRepository } from "@/domain/repositories/GetHabitProgressHistory"
import { SupabaseRepository } from "./_SupabaseRepository"
import { HabitProgress } from "@/domain/entities/HabitProgress"
import type { GoalProgress } from "@/domain/entities/Goal"
import {
GoalBooleanProgress,
GoalNumericProgress,
} from "@/domain/entities/Goal"
import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository"
import { habitProgressHistorySupabaseDTO } from "../data-transfer-objects/HabitProgressDTO"
export class GetHabitProgressHistorySupabaseRepository
extends SupabaseRepository
@ -15,37 +10,15 @@ export class GetHabitProgressHistorySupabaseRepository
options,
) => {
const { habit } = options
const { data, error } = await this.supabaseClient
const { data } = await this.supabaseClient
.from("habits_progresses")
.select("*")
.eq("habit_id", habit.id)
if (error != null) {
throw new Error(error.message)
}
const habitProgressHistory = data.map((item) => {
let goalProgress: GoalProgress | null = null
if (habit.goal.isNumeric()) {
goalProgress = new GoalNumericProgress({
goal: habit.goal,
progress: item.goal_progress,
})
} else if (habit.goal.isBoolean()) {
goalProgress = new GoalBooleanProgress({
goal: habit.goal,
progress: item.goal_progress === 1,
})
}
if (goalProgress == null) {
throw new Error("Goal progress is null.")
}
const habitProgress = new HabitProgress({
id: item.id.toString(),
habitId: item.habit_id.toString(),
goalProgress,
date: new Date(item.date),
})
return habitProgress
})
return habitProgressHistory
.throwOnError()
const habitProgressHistory = data as NonNullable<typeof data>
return habitProgressHistorySupabaseDTO.fromSupabaseToDomain(
habitProgressHistory,
habit.goal,
)
}
}

View File

@ -1,8 +1,6 @@
import type { GetHabitsByUserIdRepository } from "@/domain/repositories/GetHabitsByUserId"
import { SupabaseRepository } from "./_SupabaseRepository"
import { Habit } from "@/domain/entities/Habit"
import type { Goal } from "@/domain/entities/Goal"
import { GoalBoolean, GoalNumeric } from "@/domain/entities/Goal"
import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository"
import { habitsSupabaseDTO } from "../data-transfer-objects/HabitDTO"
export class GetHabitsByUserIdSupabaseRepository
extends SupabaseRepository
@ -10,39 +8,12 @@ export class GetHabitsByUserIdSupabaseRepository
{
public execute: GetHabitsByUserIdRepository["execute"] = async (options) => {
const { userId } = options
const { data, error } = await this.supabaseClient
const { data } = await this.supabaseClient
.from("habits")
.select("*")
.eq("user_id", userId)
if (error != null) {
throw new Error(error.message)
}
return data.map((item) => {
let goal: Goal
if (item.goal_target != null && item.goal_target_unit != null) {
goal = new GoalNumeric({
frequency: item.goal_frequency,
target: {
value: item.goal_target,
unit: item.goal_target_unit,
},
})
} else {
goal = new GoalBoolean({
frequency: item.goal_frequency,
})
}
const habit = new Habit({
id: item.id.toString(),
name: item.name,
color: item.color,
icon: item.icon,
userId: item.user_id.toString(),
startDate: new Date(item.start_date),
endDate: item.end_date != null ? new Date(item.end_date) : undefined,
goal,
})
return habit
})
.throwOnError()
const habits = data as NonNullable<typeof data>
return habitsSupabaseDTO.fromSupabaseToDomain(habits)
}
}

View File

@ -1,7 +1,6 @@
import { Habit } from "@/domain/entities/Habit"
import type { HabitCreateRepository } from "@/domain/repositories/HabitCreate"
import { SupabaseRepository } from "./_SupabaseRepository"
import { Goal } from "@/domain/entities/Goal"
import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository"
import { habitSupabaseDTO } from "../data-transfer-objects/HabitDTO"
export class HabitCreateSupabaseRepository
extends SupabaseRepository
@ -9,34 +8,15 @@ export class HabitCreateSupabaseRepository
{
public execute: HabitCreateRepository["execute"] = async (options) => {
const { habitCreateData } = options
const { data, error } = await this.supabaseClient
const { data } = await this.supabaseClient
.from("habits")
.insert({
name: habitCreateData.name,
color: habitCreateData.color,
icon: habitCreateData.icon,
goal_frequency: habitCreateData.goal.frequency,
...(habitCreateData.goal.target.type === "numeric"
? {
goal_target: habitCreateData.goal.target.value,
goal_target_unit: habitCreateData.goal.target.unit,
}
: {}),
})
.insert(
habitSupabaseDTO.fromDomainCreateDataToSupabaseInsert(habitCreateData),
)
.select("*")
const insertedHabit = data?.[0]
if (error != null || insertedHabit == null) {
throw new Error(error?.message ?? "Failed to create habit.")
}
const habit = new Habit({
id: insertedHabit.id.toString(),
userId: insertedHabit.user_id.toString(),
name: insertedHabit.name,
icon: insertedHabit.icon,
goal: Goal.create(habitCreateData.goal),
color: insertedHabit.color,
startDate: new Date(insertedHabit.start_date),
})
return habit
.single()
.throwOnError()
const insertedHabit = data as NonNullable<typeof data>
return habitSupabaseDTO.fromSupabaseToDomain(insertedHabit)
}
}

View File

@ -1,7 +1,6 @@
import { Habit } from "@/domain/entities/Habit"
import type { HabitEditRepository } from "@/domain/repositories/HabitEdit"
import { SupabaseRepository } from "./_SupabaseRepository"
import { Goal } from "@/domain/entities/Goal"
import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository"
import { habitSupabaseDTO } from "../data-transfer-objects/HabitDTO"
export class HabitEditSupabaseRepository
extends SupabaseRepository
@ -9,46 +8,16 @@ export class HabitEditSupabaseRepository
{
public execute: HabitEditRepository["execute"] = async (options) => {
const { habitEditData } = options
const { data, error } = await this.supabaseClient
const { data } = await this.supabaseClient
.from("habits")
.update({
name: habitEditData.name,
color: habitEditData.color,
icon: habitEditData.icon,
end_date: habitEditData?.endDate?.toISOString(),
})
.update(
habitSupabaseDTO.fromDomainEditDataToSupabaseUpdate(habitEditData),
)
.eq("id", habitEditData.id)
.select("*")
const updatedHabit = data?.[0]
if (error != null || updatedHabit == null) {
throw new Error(error?.message ?? "Failed to edit habit.")
}
const habit = new Habit({
id: updatedHabit.id.toString(),
userId: updatedHabit.user_id.toString(),
name: updatedHabit.name,
icon: updatedHabit.icon,
goal: Goal.create({
frequency: updatedHabit.goal_frequency,
target:
updatedHabit.goal_target != null &&
updatedHabit.goal_target_unit != null
? {
type: "numeric",
value: updatedHabit.goal_target,
unit: updatedHabit.goal_target_unit,
}
: {
type: "boolean",
},
}),
color: updatedHabit.color,
startDate: new Date(updatedHabit.start_date),
endDate:
updatedHabit.end_date != null
? new Date(updatedHabit.end_date)
: undefined,
})
return habit
.single()
.throwOnError()
const updatedHabit = data as NonNullable<typeof data>
return habitSupabaseDTO.fromSupabaseToDomain(updatedHabit)
}
}

View File

@ -1,6 +1,6 @@
import type { HabitProgressCreateRepository } from "@/domain/repositories/HabitProgressCreate"
import { SupabaseRepository } from "./_SupabaseRepository"
import { HabitProgress } from "@/domain/entities/HabitProgress"
import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository"
import { habitProgressSupabaseDTO } from "../data-transfer-objects/HabitProgressDTO"
export class HabitProgressCreateSupabaseRepository
extends SupabaseRepository
@ -10,29 +10,20 @@ export class HabitProgressCreateSupabaseRepository
options,
) => {
const { habitProgressData } = options
const { goalProgress, date, habitId } = habitProgressData
let goalProgressValue = goalProgress.isCompleted() ? 1 : 0
if (goalProgress.isNumeric()) {
goalProgressValue = goalProgress.progress
}
const { data, error } = await this.supabaseClient
const { data } = await this.supabaseClient
.from("habits_progresses")
.insert({
habit_id: Number(habitId),
date: date.toISOString(),
goal_progress: goalProgressValue,
})
.insert(
habitProgressSupabaseDTO.fromDomainDataToSupabaseInsert(
habitProgressData,
),
)
.select("*")
const insertedProgress = data?.[0]
if (error != null || insertedProgress == null) {
throw new Error(error?.message ?? "Failed to create habit progress.")
}
const habitProgress = new HabitProgress({
id: insertedProgress.id.toString(),
habitId: insertedProgress.habit_id.toString(),
date: new Date(insertedProgress.date),
goalProgress,
})
return habitProgress
.single()
.throwOnError()
const insertedProgress = data as NonNullable<typeof data>
return habitProgressSupabaseDTO.fromSupabaseToDomain(
insertedProgress,
habitProgressData.goalProgress.goal,
)
}
}

View File

@ -1,6 +1,6 @@
import type { HabitProgressUpdateRepository } from "@/domain/repositories/HabitProgressUpdate"
import { SupabaseRepository } from "./_SupabaseRepository"
import { HabitProgress } from "@/domain/entities/HabitProgress"
import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository"
import { habitProgressSupabaseDTO } from "../data-transfer-objects/HabitProgressDTO"
export class HabitProgressUpdateSupabaseRepository
extends SupabaseRepository
@ -10,29 +10,21 @@ export class HabitProgressUpdateSupabaseRepository
options,
) => {
const { habitProgressData } = options
const { id, goalProgress, date } = habitProgressData
let goalProgressValue = goalProgress.isCompleted() ? 1 : 0
if (goalProgress.isNumeric()) {
goalProgressValue = goalProgress.progress
}
const { data, error } = await this.supabaseClient
const { data } = await this.supabaseClient
.from("habits_progresses")
.update({
date: date.toISOString(),
goal_progress: goalProgressValue,
})
.eq("id", id)
.update(
habitProgressSupabaseDTO.fromDomainDataToSupabaseUpdate(
habitProgressData,
),
)
.eq("id", habitProgressData.id)
.select("*")
const insertedProgress = data?.[0]
if (error != null || insertedProgress == null) {
throw new Error(error?.message ?? "Failed to update habit progress.")
}
const habitProgress = new HabitProgress({
id: insertedProgress.id.toString(),
habitId: insertedProgress.habit_id.toString(),
date: new Date(insertedProgress.date),
goalProgress,
})
return habitProgress
.single()
.throwOnError()
const insertedProgress = data as NonNullable<typeof data>
return habitProgressSupabaseDTO.fromSupabaseToDomain(
insertedProgress,
habitProgressData.goalProgress.goal,
)
}
}

View File

@ -9,9 +9,19 @@ 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 SupabaseHabitInsert =
Database["public"]["Tables"]["habits"]["Insert"]
export type SupabaseHabitUpdate =
Database["public"]["Tables"]["habits"]["Update"]
export type SupabaseHabitProgress =
Database["public"]["Tables"]["habits_progresses"]["Row"]
export type SupabaseHabitProgressInsert =
Database["public"]["Tables"]["habits_progresses"]["Insert"]
export type SupabaseHabitProgressUpdate =
Database["public"]["Tables"]["habits_progresses"]["Update"]
const SUPABASE_URL =
process.env["EXPO_PUBLIC_SUPABASE_URL"] ??

View File

@ -10,7 +10,13 @@
"coverageReporters": ["text", "text-summary", "cobertura"],
"collectCoverageFrom": [
"<rootDir>/**/*.{ts,tsx}",
"!<rootDir>/tests/**/*",
"!<rootDir>/domain/repositories/**/*",
"!<rootDir>/infrastructure/instances.ts",
"!<rootDir>/infrastructure/supabase/supabase-types.ts",
"!<rootDir>/infrastructure/supabase/supabase.ts",
"!<rootDir>/presentation/react-native/ui/ExternalLink.tsx",
"!<rootDir>/presentation/react/contexts/**/*",
"!<rootDir>/.expo",
"!<rootDir>/app/+html.tsx",
"!<rootDir>/app/**/_layout.tsx",

View File

@ -41,11 +41,8 @@ export abstract class Presenter<State> {
public unsubscribe(listener: Listener<State>): void {
const listenerIndex = this._listeners.indexOf(listener)
const listenerFound = listenerIndex !== -1
if (listenerFound) {
this._listeners.splice(listenerIndex, 1)
}
}
private notifyListeners(): void {
for (const listener of this._listeners) {

View File

@ -1,9 +1,9 @@
import type { IconName } from "@fortawesome/fontawesome-svg-core"
import { fas } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"
import { memo, useCallback, useEffect, useState, useTransition } from "react"
import { Modal, ScrollView, View } from "react-native"
import { Button, List, Text, TextInput } from "react-native-paper"
import type { IconName } from "@fortawesome/fontawesome-svg-core"
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"
import { IconsList } from "./IconsList"

View File

@ -1,16 +1,16 @@
import type { IconName } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"
import { useRouter } from "expo-router"
import type LottieView from "lottie-react-native"
import { useState } from "react"
import { View } from "react-native"
import { Checkbox, List, Text } from "react-native-paper"
import type LottieView from "lottie-react-native"
import type { IconName } from "@fortawesome/free-solid-svg-icons"
import type { GoalBoolean } from "@/domain/entities/Goal"
import { GoalBooleanProgress } from "@/domain/entities/Goal"
import type { HabitHistory } from "@/domain/entities/HabitHistory"
import { getColorRGBAFromHex } from "@/utils/colors"
import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
import { getColorRGBAFromHex } from "@/utils/colors"
export interface HabitCardProps {
habitHistory: HabitHistory

View File

@ -5,8 +5,8 @@ import { Divider, List } from "react-native-paper"
import type { GoalFrequency } from "@/domain/entities/Goal"
import type { HabitsTracker } from "@/domain/entities/HabitsTracker"
import confettiJSON from "../../../assets/confetti.json"
import { capitalize } from "@/utils/strings"
import confettiJSON from "../../../assets/confetti.json"
import { HabitCard } from "./HabitCard"
export interface HabitsListProps {

View File

@ -1,11 +1,11 @@
import { createContext, useContext, useEffect } from "react"
import { usePresenterState } from "@/presentation/react/hooks/usePresenterState"
import { authenticationPresenter } from "@/infrastructure/instances"
import type {
AuthenticationPresenter,
AuthenticationPresenterState,
} from "@/presentation/presenters/Authentication"
import { authenticationPresenter } from "@/infrastructure/instances"
import { usePresenterState } from "@/presentation/react/hooks/usePresenterState"
export interface AuthenticationContextValue
extends AuthenticationPresenterState {

View File

@ -1,11 +1,11 @@
import { createContext, useContext, useEffect } from "react"
import { habitsTrackerPresenter } from "@/infrastructure/instances"
import type {
HabitsTrackerPresenter,
HabitsTrackerPresenterState,
} from "@/presentation/presenters/HabitsTracker"
import { usePresenterState } from "@/presentation/react/hooks/usePresenterState"
import { habitsTrackerPresenter } from "@/infrastructure/instances"
import { useAuthentication } from "./Authentication"
export interface HabitsTrackerContextValue extends HabitsTrackerPresenterState {

View File

@ -1,7 +1,7 @@
import { act, renderHook } from "@testing-library/react-native"
import { usePresenterState } from "@/presentation/react/hooks/usePresenterState"
import { Presenter } from "@/presentation/presenters/_Presenter"
import { usePresenterState } from "@/presentation/react/hooks/usePresenterState"
interface MockCountPresenterState {
count: number

115
tests/mocks/domain/Habit.ts Normal file
View File

@ -0,0 +1,115 @@
import { GoalBoolean, GoalNumeric } from "@/domain/entities/Goal"
import type { HabitData } from "@/domain/entities/Habit"
import { Habit } from "@/domain/entities/Habit"
import { USER_MOCK } from "./User"
import { ONE_DAY_MILLISECONDS } from "@/utils/dates"
interface HabitMockCreateOptions extends Omit<HabitData, "startDate"> {
startDate?: Date
}
const habitMockCreate = (options: HabitMockCreateOptions): Habit => {
const {
id,
userId,
name,
color,
icon,
goal,
startDate = new Date(),
endDate,
} = options
return new Habit({
id,
userId,
name,
color,
icon,
goal,
startDate,
endDate,
})
}
const examplesByNames = {
"Wake up at 07h00": habitMockCreate({
id: "1",
userId: USER_MOCK.example.id,
name: "Wake up at 07h00",
color: "#006CFF",
icon: "bed",
goal: new GoalBoolean({
frequency: "daily",
}),
}),
"Learn English": habitMockCreate({
id: "2",
userId: USER_MOCK.example.id,
name: "Learn English",
color: "#EB4034",
icon: "language",
goal: new GoalNumeric({
frequency: "daily",
target: {
value: 30,
unit: "minutes",
},
}),
}),
Walk: habitMockCreate({
id: "3",
userId: USER_MOCK.example.id,
name: "Walk",
color: "#228B22",
icon: "person-walking",
goal: new GoalNumeric({
frequency: "daily",
target: {
value: 5000,
unit: "steps",
},
}),
}),
"Clean the house": habitMockCreate({
id: "4",
userId: USER_MOCK.example.id,
name: "Clean the house",
color: "#808080",
icon: "broom",
goal: new GoalBoolean({
frequency: "weekly",
}),
}),
"Solve Programming Challenges": habitMockCreate({
id: "5",
userId: USER_MOCK.example.id,
name: "Solve Programming Challenges",
color: "#DE3163",
icon: "code",
goal: new GoalNumeric({
frequency: "monthly",
target: {
value: 5,
unit: "challenges",
},
}),
endDate: new Date(Date.now() + ONE_DAY_MILLISECONDS),
}),
} as const
export const examplesByIds = {
[examplesByNames["Wake up at 07h00"].id]: examplesByNames["Wake up at 07h00"],
[examplesByNames["Learn English"].id]: examplesByNames["Learn English"],
[examplesByNames.Walk.id]: examplesByNames.Walk,
[examplesByNames["Clean the house"].id]: examplesByNames["Clean the house"],
[examplesByNames["Solve Programming Challenges"].id]:
examplesByNames["Solve Programming Challenges"],
} as const
export const HABIT_MOCK = {
create: habitMockCreate,
example: examplesByNames["Wake up at 07h00"],
examplesByNames,
examplesByIds,
examples: Object.values(examplesByNames),
}

View File

@ -0,0 +1,51 @@
import type { GoalBoolean, GoalNumeric } from "@/domain/entities/Goal"
import {
GoalBooleanProgress,
GoalNumericProgress,
} from "@/domain/entities/Goal"
import type { HabitProgressData } from "@/domain/entities/HabitProgress"
import { HabitProgress } from "@/domain/entities/HabitProgress"
import { HABIT_MOCK } from "./Habit"
interface HabitProgressMockCreateOptions
extends Omit<HabitProgressData, "date"> {
date?: Date
}
const habitProgressMockCreate = (
options: HabitProgressMockCreateOptions,
): HabitProgress => {
const { id, habitId, goalProgress, date = new Date() } = options
return new HabitProgress({
date,
goalProgress,
habitId,
id,
})
}
const exampleByIds = {
1: habitProgressMockCreate({
id: "1",
habitId: HABIT_MOCK.examplesByNames["Clean the house"].id,
goalProgress: new GoalBooleanProgress({
goal: HABIT_MOCK.examplesByNames["Clean the house"].goal as GoalBoolean,
progress: true,
}),
}),
2: habitProgressMockCreate({
id: "2",
habitId: HABIT_MOCK.examplesByNames.Walk.id,
goalProgress: new GoalNumericProgress({
goal: HABIT_MOCK.examplesByNames.Walk.goal as GoalNumeric,
progress: 4_733,
}),
}),
} as const
export const HABIT_PROGRESS_MOCK = {
create: habitProgressMockCreate,
exampleByIds,
examples: Object.values(exampleByIds),
}

View File

@ -0,0 +1,30 @@
import type { UserData } from "@/domain/entities/User"
import { User } from "@/domain/entities/User"
const USER_MOCK_ID = "ab054ee9-fbb4-473e-942b-bbf4415f4bef"
const USER_MOCK_EMAIL = "test@test.com"
const USER_MOCK_DISPLAY_NAME = "Test"
interface UserMockCreateOptions {
id?: UserData["id"]
email?: UserData["email"]
displayName?: UserData["displayName"]
}
const userMockCreate = (options: UserMockCreateOptions = {}): User => {
const {
id = USER_MOCK_ID,
email = USER_MOCK_EMAIL,
displayName = USER_MOCK_DISPLAY_NAME,
} = options
return new User({
id,
email,
displayName,
})
}
export const USER_MOCK = {
create: userMockCreate,
example: userMockCreate(),
}

View File

@ -0,0 +1,79 @@
import type { SupabaseHabit } from "@/infrastructure/supabase/supabase"
import { HABIT_MOCK } from "../domain/Habit"
import { SUPABASE_USER_MOCK } from "./User"
interface SupabaseHabitMockCreateOptions {
id: SupabaseHabit["id"]
userId: SupabaseHabit["user_id"]
name: SupabaseHabit["name"]
color: SupabaseHabit["color"]
icon: SupabaseHabit["icon"]
startDate?: Date
endDate: Date | null
goalFrequency: SupabaseHabit["goal_frequency"]
goalTarget: SupabaseHabit["goal_target"] | null
goalTargetUnit: SupabaseHabit["goal_target_unit"] | null
}
const supabaseHabitMockCreate = (
options: SupabaseHabitMockCreateOptions,
): SupabaseHabit => {
const {
id,
userId,
name,
color,
icon,
startDate = new Date(),
endDate,
goalFrequency,
goalTarget,
goalTargetUnit,
} = options
return {
id,
user_id: userId,
name,
color,
icon,
start_date: startDate.toISOString(),
end_date: endDate?.toISOString() ?? null,
goal_frequency: goalFrequency,
goal_target: goalTarget,
goal_target_unit: goalTargetUnit,
}
}
const examplesByNames = Object.fromEntries(
Object.entries(HABIT_MOCK.examplesByNames).map(([name, habit]) => {
const goalTarget = habit.goal.isNumeric() ? habit.goal.target.value : null
const goalTargetUnit = habit.goal.isNumeric()
? habit.goal.target.unit
: null
return [
name,
supabaseHabitMockCreate({
id: Number.parseInt(habit.id, 10),
userId: SUPABASE_USER_MOCK.example.id,
name: habit.name,
color: habit.color,
icon: habit.icon,
startDate: habit.startDate,
endDate: habit.endDate ?? null,
goalFrequency: habit.goal.frequency,
goalTarget,
goalTargetUnit,
}),
]
}),
) as {
[key in keyof (typeof HABIT_MOCK)["examplesByNames"]]: SupabaseHabit
}
export const SUPABASE_HABIT_MOCK = {
create: supabaseHabitMockCreate,
example:
examplesByNames[HABIT_MOCK.example.name as keyof typeof examplesByNames],
examples: Object.values(examplesByNames),
examplesByNames,
}

View File

@ -0,0 +1,49 @@
import type { SupabaseHabitProgress } from "@/infrastructure/supabase/supabase"
import { HABIT_PROGRESS_MOCK } from "../domain/HabitProgress"
interface SupabaseHabitProgressMockCreateOptions {
id: SupabaseHabitProgress["id"]
habitId: SupabaseHabitProgress["habit_id"]
date?: Date
goalProgress: SupabaseHabitProgress["goal_progress"]
}
const supabaseHabitProgressMockCreate = (
options: SupabaseHabitProgressMockCreateOptions,
): SupabaseHabitProgress => {
const { id, habitId, date = new Date(), goalProgress } = options
return {
id,
habit_id: habitId,
date: date.toISOString(),
goal_progress: goalProgress,
}
}
const exampleByIds = Object.fromEntries(
Object.entries(HABIT_PROGRESS_MOCK.exampleByIds).map(
([id, habitProgress]) => {
return [
id,
supabaseHabitProgressMockCreate({
id: Number.parseInt(habitProgress.id, 10),
habitId: Number.parseInt(habitProgress.habitId, 10),
date: new Date(habitProgress.date),
goalProgress: habitProgress.goalProgress.isNumeric()
? habitProgress.goalProgress.progress
: habitProgress.goalProgress.isCompleted()
? 1
: 0,
}),
]
},
),
) as {
[key in keyof (typeof HABIT_PROGRESS_MOCK)["exampleByIds"]]: SupabaseHabitProgress
}
export const SUPABASE_HABIT_PROGRESS_MOCK = {
create: supabaseHabitProgressMockCreate,
exampleByIds,
examples: Object.values(exampleByIds),
}

View File

@ -0,0 +1,63 @@
import type { SupabaseUser } from "@/infrastructure/supabase/supabase"
import { USER_MOCK } from "../domain/User"
interface SupabaseUserMockCreateOptions {
id?: SupabaseUser["id"]
email?: SupabaseUser["email"]
displayName?: SupabaseUser["user_metadata"]["display_name"]
date?: Date
}
const supabaseUserMockCreate = (
options: SupabaseUserMockCreateOptions = {},
): SupabaseUser => {
const {
id = USER_MOCK.example.id,
email = USER_MOCK.example.email,
displayName = USER_MOCK.example.displayName,
date = new Date(),
} = options
return {
id,
app_metadata: { provider: "email", providers: ["email"] },
user_metadata: { display_name: displayName },
aud: "authenticated",
email,
confirmation_sent_at: undefined,
recovery_sent_at: undefined,
email_change_sent_at: undefined,
new_email: "",
new_phone: "",
invited_at: undefined,
action_link: "",
created_at: date.toISOString(),
confirmed_at: undefined,
email_confirmed_at: date.toISOString(),
phone_confirmed_at: undefined,
last_sign_in_at: undefined,
role: "authenticated",
updated_at: date.toISOString(),
identities: [
{
id,
user_id: id,
identity_data: {
sub: id,
email,
},
provider: "email",
identity_id: id,
last_sign_in_at: date.toISOString(),
created_at: date.toISOString(),
updated_at: date.toISOString(),
},
],
is_anonymous: false,
factors: [],
}
}
export const SUPABASE_USER_MOCK = {
create: supabaseUserMockCreate,
example: supabaseUserMockCreate(),
}