crud tests for subjects, assignments and tasks
This commit is contained in:
73
__tests__/assignment/createAssignment.test.tsx
Normal file
73
__tests__/assignment/createAssignment.test.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
||||
import { router } from "expo-router";
|
||||
import CreateAssignment from "../../app/assignment/createAssignment";
|
||||
|
||||
const mockSingle = jest.fn();
|
||||
const mockSelect = jest.fn(() => ({ single: mockSingle, }));
|
||||
const mockInsert = jest.fn(() => ({ select: mockSelect, }));
|
||||
|
||||
jest.mock("expo-router", () => ({
|
||||
router: {
|
||||
back: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
},
|
||||
Stack: {
|
||||
Screen: () => null,
|
||||
},
|
||||
useLocalSearchParams: () => ({
|
||||
sId: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/progress", () => ({
|
||||
CheckAssignmentCompletion: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/asyncStorage", () => ({
|
||||
SaveAssignmentNotificationId: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("expo-notifications", () => ({
|
||||
scheduleNotificationAsync: jest.fn(() => Promise.resolve("notification-123")),
|
||||
SchedulableTriggerInputTypes: {
|
||||
DATE: "date",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/supabase", () => {
|
||||
return {
|
||||
supabase: {
|
||||
auth: {
|
||||
getUser: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { user: { id: "user-123" } },
|
||||
error: null,
|
||||
})
|
||||
),
|
||||
},
|
||||
from: jest.fn(() => ({
|
||||
insert: mockInsert,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test("creates an assignment and navigates back", async () => {
|
||||
mockSingle.mockResolvedValue({
|
||||
data: {
|
||||
aId: "assignment-123", title: "create a simple test", deadline: "",
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
|
||||
const screen = render(<CreateAssignment />);
|
||||
fireEvent.changeText(screen.getByTestId("assignment-title-input"), "create a simple test");
|
||||
fireEvent.press(screen.getByTestId("create-assignment-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(supabase.from).toHaveBeenCalledWith("assignments");
|
||||
expect(mockInsert).toHaveBeenCalled();
|
||||
expect(router.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
110
__tests__/assignment/deleteAssignment.test.tsx
Normal file
110
__tests__/assignment/deleteAssignment.test.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
||||
import { router } from "expo-router";
|
||||
import { Alert } from "react-native";
|
||||
import ViewDetailsAssignment from "../../app/assignment/viewDetailsAssignment";
|
||||
|
||||
const mockSingleAssignment = jest.fn();
|
||||
const mockSelectAssignmentEq = jest.fn(() => ({ single: mockSingleAssignment, }));
|
||||
const mockSelectAssignment = jest.fn(() => ({ eq: mockSelectAssignmentEq, }));
|
||||
const mockSelectTasksEq = jest.fn();
|
||||
const mockSelectTasks = jest.fn(() => ({ eq: mockSelectTasksEq }));
|
||||
const mockDeleteAssignmentEq = jest.fn();
|
||||
const mockDeleteAssignment = jest.fn(() => ({ eq: mockDeleteAssignmentEq, }));
|
||||
|
||||
jest.mock("expo-router", () => ({
|
||||
router: {
|
||||
back: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
},
|
||||
Stack: {
|
||||
Screen: () => null,
|
||||
},
|
||||
useLocalSearchParams: () => ({
|
||||
aId: "assignment-123",
|
||||
}),
|
||||
useFocusEffect: (callback: () => void) => callback(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/supabase", () => {
|
||||
return {
|
||||
supabase: {
|
||||
auth: {
|
||||
getUser: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { user: { uId: "user-123" } },
|
||||
error: null,
|
||||
})
|
||||
),
|
||||
getSession: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
session: {
|
||||
user: { uId: "user-123" },
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
onAuthStateChange: jest.fn(() => ({
|
||||
data: {
|
||||
subscription: {
|
||||
unsubscribe: jest.fn(),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
from: jest.fn((table) => {
|
||||
if (table === "assignments") {
|
||||
return {
|
||||
select: mockSelectAssignment,
|
||||
delete: mockDeleteAssignment,
|
||||
};
|
||||
}
|
||||
|
||||
if (table === "tasks") {
|
||||
return {
|
||||
select: mockSelectTasks,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const alertSpy = jest.spyOn(Alert, "alert");
|
||||
|
||||
test("deletes a task and navigates back", async () => {
|
||||
mockSingleAssignment.mockResolvedValue({
|
||||
data: {
|
||||
aId: "assignment-123",
|
||||
title: "create a simple test",
|
||||
uId: "user-123",
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
mockSelectTasksEq.mockResolvedValue({ data: [], error: null, })
|
||||
mockDeleteAssignmentEq.mockResolvedValue({ error: null, });
|
||||
|
||||
const screen = render(<ViewDetailsAssignment />);
|
||||
fireEvent.press(await screen.findByTestId("delete-assignment-button"));
|
||||
|
||||
expect(alertSpy).toHaveBeenCalledWith(
|
||||
"Delete Assignment",
|
||||
"Are you sure you want to delete this assignment?",
|
||||
expect.any(Array),
|
||||
);
|
||||
|
||||
const alertButtons = alertSpy.mock.calls[0][2];
|
||||
const confirmDeleteButton = alertButtons[1];
|
||||
|
||||
await confirmDeleteButton.onPress();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(supabase.from).toHaveBeenCalledWith("assignments");
|
||||
expect(mockDeleteAssignment).toHaveBeenCalled();
|
||||
expect(mockDeleteAssignmentEq).toHaveBeenCalledWith("aId", "assignment-123");
|
||||
expect(router.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
99
__tests__/assignment/editAssignment.test.tsx
Normal file
99
__tests__/assignment/editAssignment.test.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import EditAssignment from "@/app/assignment/editAssignment";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
||||
import { router } from "expo-router";
|
||||
|
||||
const mockUpdateSingle = jest.fn();
|
||||
const mockUpdateSelect = jest.fn(() => ({ single: mockUpdateSingle, }));
|
||||
const mockUpdateEq = jest.fn(() => ({ select: mockUpdateSelect, }));
|
||||
const mockUpdate = jest.fn(() => ({ eq: mockUpdateEq, }));
|
||||
const mockSingle = jest.fn();
|
||||
const mockSelectEq = jest.fn(() => ({ single: mockSingle, }));
|
||||
const mockSelect = jest.fn(() => ({ eq: mockSelectEq, }));
|
||||
|
||||
jest.mock("expo-router", () => ({
|
||||
router: {
|
||||
back: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
},
|
||||
Stack: {
|
||||
Screen: () => null,
|
||||
},
|
||||
useLocalSearchParams: () => ({
|
||||
aId: "assignment-123",
|
||||
}),
|
||||
useFocusEffect: (callback: () => void) => callback(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/progress", () => ({
|
||||
CheckAssignmentCompletion: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/asyncStorage", () => ({
|
||||
GetAssignmentNotificationId: jest.fn(() => Promise.resolve(null)),
|
||||
}));
|
||||
|
||||
jest.mock("expo-notifications", () => ({
|
||||
scheduleNotificationAsync: jest.fn(() => Promise.resolve("notification-123")),
|
||||
SchedulableTriggerInputTypes: {
|
||||
DATE: "date",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/supabase", () => {
|
||||
return {
|
||||
supabase: {
|
||||
auth: {
|
||||
getUser: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { user: { id: "user-123" } },
|
||||
error: null,
|
||||
})
|
||||
),
|
||||
},
|
||||
from: jest.fn(() => ({
|
||||
select: mockSelect,
|
||||
update: mockUpdate,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test("updates an assignment and navigates back", async () => {
|
||||
mockSingle.mockResolvedValue({
|
||||
data: {
|
||||
aId: "assignment-123",
|
||||
title: "create a simple test",
|
||||
uId: "user-123",
|
||||
deadline: "2026-04-25",
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
mockUpdateSingle.mockResolvedValue({
|
||||
data: {
|
||||
aId: "assignment-123",
|
||||
title: "create a harder test",
|
||||
uId: "user-123",
|
||||
deadline: "2026-04-25",
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
|
||||
const screen = render(<EditAssignment />);
|
||||
fireEvent.changeText(await screen.findByTestId("assignment-title-input"), "create a harder test");
|
||||
fireEvent.press(screen.getByTestId("edit-assignment-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(supabase.from).toHaveBeenCalledWith("assignments");
|
||||
expect(mockSelect).toHaveBeenCalled();
|
||||
expect(mockUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "create a harder test",
|
||||
uId: "user-123",
|
||||
deadline: "2026-04-25",
|
||||
})
|
||||
);
|
||||
expect(mockUpdateSingle).toHaveBeenCalled();
|
||||
expect(router.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
48
__tests__/subject/createSubject.test.tsx
Normal file
48
__tests__/subject/createSubject.test.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
||||
import { router } from "expo-router";
|
||||
import CreateSubject from "../../app/subject/createSubject";
|
||||
|
||||
const mockInsert = jest.fn();
|
||||
|
||||
jest.mock("expo-router", () => ({
|
||||
router: {
|
||||
back: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
},
|
||||
Stack: {
|
||||
Screen: () => null,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/supabase", () => {
|
||||
return {
|
||||
supabase: {
|
||||
auth: {
|
||||
getUser: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { user: { id: "user-123" } },
|
||||
error: null,
|
||||
})
|
||||
),
|
||||
},
|
||||
from: jest.fn(() => ({
|
||||
insert: mockInsert,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test("creates a subject and navigates back", async () => {
|
||||
mockInsert.mockResolvedValue({ error: null });
|
||||
|
||||
const screen = render(<CreateSubject />);
|
||||
fireEvent.changeText(screen.getByTestId("subject-title-input"), "ikt205g26v");
|
||||
fireEvent.press(screen.getByTestId("create-subject-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(supabase.from).toHaveBeenCalledWith("subjects");
|
||||
expect(mockInsert).toHaveBeenCalled();
|
||||
expect(router.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
111
__tests__/subject/deleteSubject.test.tsx
Normal file
111
__tests__/subject/deleteSubject.test.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
||||
import { router } from "expo-router";
|
||||
import { Alert } from "react-native";
|
||||
import ViewDetailsSubject from "../../app/subject/viewDetailsSubject";
|
||||
|
||||
const mockSingleSubject = jest.fn();
|
||||
const mockSelectSubjectEq = jest.fn(() => ({ single: mockSingleSubject, }));
|
||||
const mockSelectSubject = jest.fn(() => ({ eq: mockSelectSubjectEq, }));
|
||||
const mockOrderAssignments = jest.fn();
|
||||
const mockSelectAssignmentsEq = jest.fn(() => ({ order: mockOrderAssignments }));
|
||||
const mockSelectAssignments = jest.fn(() => ({ eq: mockSelectAssignmentsEq }));
|
||||
const mockDeleteSubjectEq = jest.fn();
|
||||
const mockDeleteSubject = jest.fn(() => ({ eq: mockDeleteSubjectEq, }));
|
||||
|
||||
jest.mock("expo-router", () => ({
|
||||
router: {
|
||||
back: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
},
|
||||
Stack: {
|
||||
Screen: () => null,
|
||||
},
|
||||
useLocalSearchParams: () => ({
|
||||
sId: "subject-123",
|
||||
}),
|
||||
useFocusEffect: (callback: () => void) => callback(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/supabase", () => {
|
||||
return {
|
||||
supabase: {
|
||||
auth: {
|
||||
getUser: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { user: { uId: "user-123" } },
|
||||
error: null,
|
||||
})
|
||||
),
|
||||
getSession: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
session: {
|
||||
user: { uId: "user-123" },
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
onAuthStateChange: jest.fn(() => ({
|
||||
data: {
|
||||
subscription: {
|
||||
unsubscribe: jest.fn(),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
from: jest.fn((table) => {
|
||||
if (table === "subjects") {
|
||||
return {
|
||||
select: mockSelectSubject,
|
||||
delete: mockDeleteSubject,
|
||||
};
|
||||
}
|
||||
|
||||
if (table === "assignments") {
|
||||
return {
|
||||
select: mockSelectAssignments,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const alertSpy = jest.spyOn(Alert, "alert");
|
||||
|
||||
test("deletes a subject and navigates back", async () => {
|
||||
mockSingleSubject.mockResolvedValue({
|
||||
data: {
|
||||
sId: "subject-123",
|
||||
title: "ikt205g26v",
|
||||
uId: "user-123",
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
mockOrderAssignments.mockResolvedValue({ data: [], error: null, })
|
||||
mockDeleteSubjectEq.mockResolvedValue({ error: null, });
|
||||
|
||||
const screen = render(<ViewDetailsSubject />);
|
||||
fireEvent.press(await screen.findByTestId("delete-subject-button"));
|
||||
|
||||
expect(alertSpy).toHaveBeenCalledWith(
|
||||
"Delete Subject",
|
||||
"Are you sure you want to delete this subject?",
|
||||
expect.any(Array),
|
||||
);
|
||||
|
||||
const alertButtons = alertSpy.mock.calls[0][2];
|
||||
const confirmDeleteButton = alertButtons[1];
|
||||
|
||||
await confirmDeleteButton.onPress();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(supabase.from).toHaveBeenCalledWith("subjects");
|
||||
expect(mockDeleteSubject).toHaveBeenCalled();
|
||||
expect(mockDeleteSubjectEq).toHaveBeenCalledWith("sId", "subject-123");
|
||||
expect(router.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
72
__tests__/subject/editSubject.test.tsx
Normal file
72
__tests__/subject/editSubject.test.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
||||
import { router } from "expo-router";
|
||||
import EditSubject from "../../app/subject/editSubject";
|
||||
|
||||
const mockUpdateEq = jest.fn();
|
||||
const mockUpdate = jest.fn(() => ({ eq: mockUpdateEq, }));
|
||||
const mockSingle = jest.fn();
|
||||
const mockSelectEq = jest.fn(() => ({ single: mockSingle, }));
|
||||
const mockSelect = jest.fn(() => ({ eq: mockSelectEq, }));
|
||||
|
||||
jest.mock("expo-router", () => ({
|
||||
router: {
|
||||
back: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
},
|
||||
Stack: {
|
||||
Screen: () => null,
|
||||
},
|
||||
useLocalSearchParams: () => ({
|
||||
sId: "subject-123",
|
||||
}),
|
||||
useFocusEffect: (callback: () => void) => callback(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/supabase", () => {
|
||||
return {
|
||||
supabase: {
|
||||
auth: {
|
||||
getUser: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { user: { id: "user-123" } },
|
||||
error: null,
|
||||
})
|
||||
),
|
||||
},
|
||||
from: jest.fn(() => ({
|
||||
select: mockSelect,
|
||||
update: mockUpdate,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test("updates a subject and navigates back", async () => {
|
||||
mockSingle.mockResolvedValue({
|
||||
data: {
|
||||
sId: "subject-123",
|
||||
title: "ikt205g26v",
|
||||
uId: "user-123",
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
mockUpdateEq.mockResolvedValue({ error: null, });
|
||||
|
||||
const screen = render(<EditSubject />);
|
||||
fireEvent.changeText(await screen.findByTestId("subject-title-input"), "ikt206g26v");
|
||||
fireEvent.press(screen.getByTestId("edit-subject-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(supabase.from).toHaveBeenCalledWith("subjects");
|
||||
expect(mockSelect).toHaveBeenCalled();
|
||||
expect(mockUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "ikt206g26v",
|
||||
uId: "user-123",
|
||||
})
|
||||
);
|
||||
expect(mockUpdateEq).toHaveBeenCalledWith("sId", "subject-123");
|
||||
expect(router.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
55
__tests__/task/createTask.test.tsx
Normal file
55
__tests__/task/createTask.test.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
||||
import { router } from "expo-router";
|
||||
import CreateTask from "../../app/task/createTask";
|
||||
|
||||
const mockInsert = jest.fn();
|
||||
|
||||
jest.mock("expo-router", () => ({
|
||||
router: {
|
||||
back: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
},
|
||||
Stack: {
|
||||
Screen: () => null,
|
||||
},
|
||||
useLocalSearchParams: () => ({
|
||||
aId: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/progress", () => ({
|
||||
CheckAssignmentCompletion: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/supabase", () => {
|
||||
return {
|
||||
supabase: {
|
||||
auth: {
|
||||
getUser: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { user: { id: "user-123" } },
|
||||
error: null,
|
||||
})
|
||||
),
|
||||
},
|
||||
from: jest.fn(() => ({
|
||||
insert: mockInsert,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test("creates a task and navigates back", async () => {
|
||||
mockInsert.mockResolvedValue({ error: null });
|
||||
|
||||
const screen = render(<CreateTask />);
|
||||
fireEvent.changeText(screen.getByTestId("task-title-input"), "Read chapter 4");
|
||||
fireEvent.press(screen.getByTestId("create-task-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(supabase.from).toHaveBeenCalledWith("tasks");
|
||||
expect(mockInsert).toHaveBeenCalled();
|
||||
expect(router.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
97
__tests__/task/deleteTask.test.tsx
Normal file
97
__tests__/task/deleteTask.test.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
||||
import { router } from "expo-router";
|
||||
import { Alert } from "react-native";
|
||||
import ViewDetailsTask from "../../app/task/viewDetailsTask";
|
||||
|
||||
const mockSingleTask = jest.fn();
|
||||
const mockSelectTaskEq = jest.fn(() => ({ single: mockSingleTask, }));
|
||||
const mockSelectTask = jest.fn(() => ({ eq: mockSelectTaskEq, }));
|
||||
const mockDeleteTaskEq = jest.fn();
|
||||
const mockDeleteTask = jest.fn(() => ({ eq: mockDeleteTaskEq, }));
|
||||
|
||||
jest.mock("expo-router", () => ({
|
||||
router: {
|
||||
back: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
},
|
||||
Stack: {
|
||||
Screen: () => null,
|
||||
},
|
||||
useLocalSearchParams: () => ({
|
||||
tId: "task-123",
|
||||
}),
|
||||
useFocusEffect: (callback: () => void) => callback(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/supabase", () => {
|
||||
return {
|
||||
supabase: {
|
||||
auth: {
|
||||
getUser: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { user: { uId: "user-123" } },
|
||||
error: null,
|
||||
})
|
||||
),
|
||||
getSession: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
session: {
|
||||
user: { uId: "user-123" },
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
onAuthStateChange: jest.fn(() => ({
|
||||
data: {
|
||||
subscription: {
|
||||
unsubscribe: jest.fn(),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
from: jest.fn(() => {
|
||||
return {
|
||||
select: mockSelectTask,
|
||||
delete: mockDeleteTask,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const alertSpy = jest.spyOn(Alert, "alert");
|
||||
|
||||
test("deletes a task and navigates back", async () => {
|
||||
mockSingleTask.mockResolvedValue({
|
||||
data: {
|
||||
tId: "task-123",
|
||||
title: "Read chapter 4",
|
||||
uId: "user-123",
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
mockDeleteTaskEq.mockResolvedValue({ error: null, });
|
||||
|
||||
const screen = render(<ViewDetailsTask />);
|
||||
fireEvent.press(await screen.findByTestId("delete-task-button"));
|
||||
|
||||
expect(alertSpy).toHaveBeenCalledWith(
|
||||
"Delete Task",
|
||||
"Are you sure you want to delete this task?",
|
||||
expect.any(Array),
|
||||
);
|
||||
|
||||
const alertButtons = alertSpy.mock.calls[0][2];
|
||||
const confirmDeleteButton = alertButtons[1];
|
||||
|
||||
await confirmDeleteButton.onPress();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(supabase.from).toHaveBeenCalledWith("tasks");
|
||||
expect(mockDeleteTask).toHaveBeenCalled();
|
||||
expect(mockDeleteTaskEq).toHaveBeenCalledWith("tId", "task-123");
|
||||
expect(router.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
76
__tests__/task/editTask.test.tsx
Normal file
76
__tests__/task/editTask.test.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
||||
import { router } from "expo-router";
|
||||
import EditTask from "../../app/task/editTask";
|
||||
|
||||
const mockUpdateEq = jest.fn();
|
||||
const mockUpdate = jest.fn(() => ({ eq: mockUpdateEq, }));
|
||||
const mockSingle = jest.fn();
|
||||
const mockSelectEq = jest.fn(() => ({ single: mockSingle, }));
|
||||
const mockSelect = jest.fn(() => ({ eq: mockSelectEq, }));
|
||||
|
||||
jest.mock("expo-router", () => ({
|
||||
router: {
|
||||
back: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
},
|
||||
Stack: {
|
||||
Screen: () => null,
|
||||
},
|
||||
useLocalSearchParams: () => ({
|
||||
tId: "task-123",
|
||||
}),
|
||||
useFocusEffect: (callback: () => void) => callback(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/progress", () => ({
|
||||
CheckAssignmentCompletion: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/supabase", () => {
|
||||
return {
|
||||
supabase: {
|
||||
auth: {
|
||||
getUser: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { user: { id: "user-123" } },
|
||||
error: null,
|
||||
})
|
||||
),
|
||||
},
|
||||
from: jest.fn(() => ({
|
||||
select: mockSelect,
|
||||
update: mockUpdate,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test("updates a task and navigates back", async () => {
|
||||
mockSingle.mockResolvedValue({
|
||||
data: {
|
||||
tId: "task-123",
|
||||
title: "Read chapter 4",
|
||||
uId: "user-123",
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
mockUpdateEq.mockResolvedValue({ error: null, });
|
||||
|
||||
const screen = render(<EditTask />);
|
||||
fireEvent.changeText(await screen.findByTestId("task-title-input"), "Read chapter 5");
|
||||
fireEvent.press(screen.getByTestId("edit-task-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(supabase.from).toHaveBeenCalledWith("tasks");
|
||||
expect(mockSelect).toHaveBeenCalled();
|
||||
expect(mockUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "Read chapter 5",
|
||||
uId: "user-123",
|
||||
})
|
||||
);
|
||||
expect(mockUpdateEq).toHaveBeenCalledWith("tId", "task-123");
|
||||
expect(router.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
7
app.json
7
app.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "Study-Sprint",
|
||||
"slug": "Study-Sprint",
|
||||
"slug": "study-sprint",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
@@ -49,8 +49,9 @@
|
||||
"extra": {
|
||||
"router": {},
|
||||
"eas": {
|
||||
"projectId": "25652385-934a-4a29-8fa7-deff3281e03e"
|
||||
"projectId": "d9e26d91-0f0c-4b97-b11a-20be2916e9f3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"owner": "ikt205g26v-g18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ export default function CreateAssignment() {
|
||||
<View className="mb-5">
|
||||
<Text className={labelClassName}>Title</Text>
|
||||
<TextInput
|
||||
testID = "assignment-title-input"
|
||||
className={inputClassName}
|
||||
placeholder="Enter assignment title"
|
||||
value={title}
|
||||
@@ -219,6 +220,7 @@ export default function CreateAssignment() {
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
testID = "create-assignment-button"
|
||||
className={`h-14 items-center justify-center rounded-2xl ${
|
||||
isSaving ? 'bg-accent-disabled' : 'bg-accent'
|
||||
}`}
|
||||
|
||||
@@ -149,6 +149,7 @@ export default function EditAssignment() {
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={defaultStyles.container}>
|
||||
<TextInput
|
||||
testID="assignment-title-input"
|
||||
style={defaultStyles.inputText}
|
||||
placeholder="Title"
|
||||
value={assignment.title}
|
||||
@@ -176,7 +177,7 @@ export default function EditAssignment() {
|
||||
<Text style={defaultStyles.checkboxLabel}>{assignment.isCompleted ? 'Completed' : 'Not Completed'}</Text>
|
||||
</Pressable>
|
||||
|
||||
<Button title={isSaving ? "Saving..." : "Save"} onPress={EditAssignment} disabled={isSaving} />
|
||||
<Button testID="edit-assignment-button" title={isSaving ? "Saving..." : "Save"} onPress={EditAssignment} disabled={isSaving} />
|
||||
{isSaving && (
|
||||
<ActivityIndicator size="large" />
|
||||
)}
|
||||
|
||||
@@ -198,7 +198,7 @@ export default function ViewDetailsAssignment() {
|
||||
</View>
|
||||
|
||||
<Button title="Edit" onPress={() => router.push({pathname: "/assignment/editAssignment", params: { aId: assignment.aId }})} />
|
||||
<Button title="Delete" onPress={() => DeleteAssignment(assignment.aId)} />
|
||||
<Button testID = "delete-assignment-button" title="Delete" onPress={() => DeleteAssignment(assignment.aId)} />
|
||||
</View>
|
||||
|
||||
<View style={defaultStyles.buttonContainer}>
|
||||
@@ -224,7 +224,7 @@ export default function ViewDetailsAssignment() {
|
||||
{isOwner && (
|
||||
<View style={defaultStyles.buttonContainer}>
|
||||
<Button title="Edit" onPress={() => router.push({pathname: "/task/editTask", params: { tId: item.tId }})} />
|
||||
<Button title="Delete" onPress={() => DeleteTask(item.tId, item.tId)} />
|
||||
<Button title="Delete" onPress={() => DeleteTask(item.tId, item.aId)} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -103,6 +103,7 @@ export default function CreateSubject() {
|
||||
<View className="mb-5">
|
||||
<Text className={labelClassName}>Title</Text>
|
||||
<TextInput
|
||||
testID="subject-title-input"
|
||||
className={inputClassName}
|
||||
placeholder="Enter subject title"
|
||||
value={title}
|
||||
@@ -157,6 +158,7 @@ export default function CreateSubject() {
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
testID="create-subject-button"
|
||||
className={`h-14 items-center justify-center rounded-2xl ${
|
||||
isSaving ? 'bg-accent-disabled' : 'bg-accent'
|
||||
}`}
|
||||
|
||||
@@ -88,6 +88,7 @@ export default function EditSubject() {
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={defaultStyles.container}>
|
||||
<TextInput
|
||||
testID="subject-title-input"
|
||||
style={defaultStyles.inputText}
|
||||
placeholder="Title"
|
||||
value={subject.title}
|
||||
@@ -109,7 +110,7 @@ export default function EditSubject() {
|
||||
<Text style={defaultStyles.checkboxLabel}>{subject.isActive ? 'Active' : 'inactive'}</Text>
|
||||
</Pressable>
|
||||
|
||||
<Button title={isSaving ? "Saving..." : "Save"} onPress={EditSubject} disabled={isSaving} />
|
||||
<Button testID="edit-subject-button" title={isSaving ? "Saving..." : "Save"} onPress={EditSubject} disabled={isSaving} />
|
||||
{isSaving && (
|
||||
<ActivityIndicator size="large" />
|
||||
)}
|
||||
|
||||
@@ -186,7 +186,7 @@ export default function ViewDetailsSubject() {
|
||||
</View>
|
||||
|
||||
<Button title="Edit" onPress={() => router.push({pathname: "/subject/editSubject", params: { sId: subject.sId }})} />
|
||||
<Button title="Delete" onPress={() => DeleteSubject(subject.sId)} />
|
||||
<Button testID = "delete-subject-button" title="Delete" onPress={() => DeleteSubject(subject.sId)} />
|
||||
|
||||
<View style={defaultStyles.buttonContainer}>
|
||||
<Button title="Create Assignment" onPress={() => router.push({pathname: "/assignment/createAssignment", params: { sId: subject.sId }})} />
|
||||
|
||||
@@ -115,6 +115,7 @@ export default function CreateTask() {
|
||||
<View className="mb-5">
|
||||
<Text className={labelClassName}>Title</Text>
|
||||
<TextInput
|
||||
testID="task-title-input"
|
||||
className={inputClassName}
|
||||
placeholder="Enter task title"
|
||||
value={title}
|
||||
@@ -169,6 +170,7 @@ export default function CreateTask() {
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
testID="create-task-button"
|
||||
className={`h-14 items-center justify-center rounded-2xl ${
|
||||
isSaving ? 'bg-accent-disabled' : 'bg-accent'
|
||||
}`}
|
||||
|
||||
@@ -98,6 +98,7 @@ export default function EditTask() {
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={defaultStyles.container}>
|
||||
<TextInput
|
||||
testID="task-title-input"
|
||||
style={defaultStyles.inputText}
|
||||
placeholder="Title"
|
||||
value={task.title}
|
||||
@@ -119,7 +120,7 @@ export default function EditTask() {
|
||||
<Text style={defaultStyles.checkboxLabel}>{task.isCompleted ? 'Completed' : 'Not Completed'}</Text>
|
||||
</Pressable>
|
||||
|
||||
<Button title={isSaving ? "Saving..." : "Save"} onPress={EditTask} disabled={isSaving} />
|
||||
<Button testID="edit-task-button" title={isSaving ? "Saving..." : "Save"} onPress={EditTask} disabled={isSaving} />
|
||||
{isSaving && (
|
||||
<ActivityIndicator size="large" />
|
||||
)}
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function ViewDetailsTask() {
|
||||
|
||||
<View style={defaultStyles.buttonContainer}>
|
||||
<Button title="Edit" onPress={() => router.push({pathname: "/task/editTask", params: { tId: task.tId }})} />
|
||||
<Button title="Delete" onPress={() => DeleteTask(task.tId)} />
|
||||
<Button testID = "delete-task-button" title="Delete" onPress={() => DeleteTask(task.tId)} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
71
notes/work-report-2026-04-24-to-25.md
Normal file
71
notes/work-report-2026-04-24-to-25.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# CRUD Testing Summary (React Native + Jest + Supabase)
|
||||
|
||||
## What these tests are about
|
||||
Tests verify **app behavior**, not Supabase itself.
|
||||
|
||||
They check:
|
||||
- User interaction works
|
||||
- Correct database functions are called
|
||||
- Navigation happens after actions
|
||||
|
||||
---
|
||||
|
||||
## CRUD Breakdown
|
||||
|
||||
### CREATE
|
||||
- User inputs data
|
||||
- `insert()` is called
|
||||
- App navigates back
|
||||
|
||||
Flow:
|
||||
User → type → press create → insert() → router.back()
|
||||
|
||||
---
|
||||
|
||||
### READ
|
||||
- Data is fetched (`select().eq().single()`)
|
||||
- State updates
|
||||
- UI renders correct content
|
||||
|
||||
---
|
||||
|
||||
### UPDATE
|
||||
- Existing data is loaded
|
||||
- User edits input
|
||||
- `update().eq()` is called with correct values
|
||||
- Navigation happens
|
||||
|
||||
---
|
||||
|
||||
### DELETE
|
||||
- User presses delete
|
||||
- `Alert.alert()` is triggered
|
||||
- Confirm button (`onPress`) is manually called in test
|
||||
- `delete().eq()` runs
|
||||
- Navigation happens
|
||||
|
||||
---
|
||||
|
||||
## Why mocking is used
|
||||
- No real database calls
|
||||
- Faster tests
|
||||
- Full control over success/error cases
|
||||
- No side effects (no real data created/deleted)
|
||||
|
||||
---
|
||||
|
||||
## Mock rule
|
||||
The mock must match the real call chain:
|
||||
|
||||
Real:
|
||||
from → update → eq → select → single
|
||||
|
||||
Mock:
|
||||
from() → update() → eq() → select() → single()
|
||||
|
||||
If not → errors like:
|
||||
".select is not a function"
|
||||
|
||||
---
|
||||
|
||||
https://oss.callstack.com/react-native-testing-library/
|
||||
2908
package-lock.json
generated
2908
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -8,7 +8,8 @@
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"lint": "expo lint"
|
||||
"lint": "expo lint",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
@@ -45,12 +46,20 @@
|
||||
"react-native-worklets": "0.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/react": "~19.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-config-expo": "~10.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "^55.0.16",
|
||||
"patch-package": "^8.0.1",
|
||||
"react-test-renderer": "19.1.0",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
"private": true,
|
||||
"jest": {
|
||||
"preset": "jest-expo"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user