Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions apps/mobile/__tests__/authenticated-header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { fireEvent, render } from "@testing-library/react-native";

import { AuthenticatedHeader } from "../features/navigation/AuthenticatedHeader";

describe("AuthenticatedHeader", () => {
it("navigates to every existing menu route and disables unfinished routes", async () => {
const replace = jest.fn();
const screen = await render(
<AuthenticatedHeader
navigation={{ replace }}
onLogout={() => replace("/")}
/>,
);

await fireEvent.press(screen.getByRole("button", { name: "Open menu" }));

for (const route of [
["Home", "/home"],
["Watched Items", "/watched-items"],
["Recommended", "/recommended"],
["Popular", "/home/popular-products"],
["Notifications", "/notifications"],
["Quick Checkout", "/quick-checkout"],
]) {
await fireEvent.press(screen.getByRole("button", { name: route[0] }));
expect(replace).toHaveBeenLastCalledWith(route[1]);
await fireEvent.press(screen.getByRole("button", { name: "Open menu" }));
}

expect(
screen.getByRole("button", { name: "Categories, Coming Soon" }).props
.accessibilityState.disabled,
).toBe(true);
expect(
screen.getByRole("button", { name: "Stores, Coming Soon" }).props
.accessibilityState.disabled,
).toBe(true);
expect(
screen.getByRole("button", { name: "Settings, Coming Soon" }).props
.accessibilityState.disabled,
).toBe(true);
});

it("signs out and navigates to login", async () => {
const replace = jest.fn();
const screen = await render(
<AuthenticatedHeader
navigation={{ replace }}
onLogout={() => replace("/")}
/>,
);

await fireEvent.press(screen.getByRole("button", { name: "Open menu" }));
await fireEvent.press(screen.getByRole("button", { name: "Log out" }));

expect(replace).toHaveBeenCalledWith("/");
});
});
59 changes: 59 additions & 0 deletions apps/mobile/__tests__/home-screen.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
jest.mock("../lib/supabase", () => ({
supabase: { auth: { signOut: jest.fn() } },
}));

jest.mock("../features/products/ProductCard", () => ({
ProductCard: ({ name }: { name: string }) => {
return <MockText testID="shared-product-card">{name}</MockText>;
},
}));

import { fireEvent, render } from "@testing-library/react-native";
import { Text as MockText } from "react-native";

import { HomeScreen } from "../features/home/HomeScreen";

describe("HomeScreen", () => {
it("renders homepage products with the shared ProductCard", async () => {
const screen = await render(
<HomeScreen onLogout={jest.fn()} onOpenNotifications={jest.fn()} />,
);

expect(screen.getAllByTestId("shared-product-card")).toHaveLength(4);
});

it("opens Watched Items from the homepage section", async () => {
const onOpenWatchedItems = jest.fn();
const screen = await render(
<HomeScreen
onLogout={jest.fn()}
onOpenNotifications={jest.fn()}
onOpenWatchedItems={onOpenWatchedItems}
/>,
);

await fireEvent.press(
screen.getByRole("button", { name: "View watched items" }),
);

expect(onOpenWatchedItems).toHaveBeenCalledTimes(1);
});

it("opens Watched Items from the menu", async () => {
const onOpenWatchedItems = jest.fn();
const screen = await render(
<HomeScreen
onLogout={jest.fn()}
onOpenNotifications={jest.fn()}
onOpenWatchedItems={onOpenWatchedItems}
/>,
);

await fireEvent.press(screen.getByRole("button", { name: "Open menu" }));
await fireEvent.press(
screen.getByRole("button", { name: "Watched Items" }),
);

expect(onOpenWatchedItems).toHaveBeenCalledTimes(1);
});
});
26 changes: 26 additions & 0 deletions apps/mobile/__tests__/popular-products-navigation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
jest.mock("../lib/supabase", () => ({
supabase: { auth: { signOut: jest.fn() } },
}));

import { fireEvent, render } from "@testing-library/react-native";

import { HomeScreen } from "../features/home/HomeScreen";

describe("HomeScreen popular products navigation", () => {
it("opens Popular Products from the homepage section", async () => {
const onOpenPopularProducts = jest.fn();
const screen = await render(
<HomeScreen
onLogout={jest.fn()}
onOpenNotifications={jest.fn()}
onOpenPopularProducts={onOpenPopularProducts}
/>,
);

await fireEvent.press(
screen.getByRole("button", { name: "View popular products" }),
);

expect(onOpenPopularProducts).toHaveBeenCalledTimes(1);
});
});
36 changes: 36 additions & 0 deletions apps/mobile/__tests__/popular-products-screen.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { fireEvent, render } from "@testing-library/react-native";

import { PopularProductsScreen } from "../features/popular-products/PopularProductsScreen";

describe("PopularProductsScreen", () => {
it("shows placeholder products with their stores and prices", async () => {
const screen = await render(<PopularProductsScreen />);

expect(screen.getByText("Nintendo Switch 2")).toBeTruthy();
expect(screen.getByText("Target")).toBeTruthy();
expect(screen.getByText("$449.99")).toBeTruthy();
});

it("filters products by search text", async () => {
const screen = await render(<PopularProductsScreen />);

await fireEvent.changeText(
screen.getByLabelText("Search popular products"),
"sony",
);

expect(screen.getByText("Sony WH-1000XM6")).toBeTruthy();
expect(screen.queryByText("Nintendo Switch 2")).toBeNull();
});

it("filters products by category", async () => {
const screen = await render(<PopularProductsScreen />);

await fireEvent.press(screen.getByRole("button", { name: "Cards" }));

expect(
screen.getByText("Pokémon TCG: Prismatic Evolutions ETB"),
).toBeTruthy();
expect(screen.queryByText("Sony WH-1000XM6")).toBeNull();
});
});
81 changes: 81 additions & 0 deletions apps/mobile/__tests__/quick-checkout-screen.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { fireEvent, render } from "@testing-library/react-native";

import { QuickCheckoutScreen } from "../features/quick-checkout/QuickCheckoutScreen";

describe("QuickCheckoutScreen", () => {
it("requires checkout information and a valid spending limit", async () => {
const screen = await render(<QuickCheckoutScreen />);

await fireEvent.press(
screen.getByRole("button", { name: "Simulate Checkout" }),
);

expect(screen.getByText("Enter your checkout information.")).toBeTruthy();

await fireEvent(
screen.getByRole("switch", { name: "Auto-fill saved information" }),
"valueChange",
true,
);
await fireEvent.press(
screen.getByRole("button", { name: "Simulate Checkout" }),
);
expect(screen.getByText("Enter a valid maximum price.")).toBeTruthy();
});

it("auto-fills saved information and toggles auto-buy", async () => {
const screen = await render(<QuickCheckoutScreen />);

const autoFill = screen.getByRole("switch", {
name: "Auto-fill saved information",
});
await fireEvent(
screen.getByRole("switch", { name: "Auto-fill saved information" }),
"valueChange",
true,
);
await fireEvent(
screen.getByRole("switch", { name: "Auto-buy simulation" }),
"valueChange",
true,
);

expect(autoFill.props.value).toBe(true);
expect(screen.getByDisplayValue("Alex Shopper")).toBeTruthy();
expect(screen.getByDisplayValue("123 Sample Street")).toBeTruthy();
expect(
screen.getByRole("switch", { name: "Auto-buy simulation" }).props.value,
).toBe(true);
});

it("succeeds when the product is within the spending limit and fails when it is not", async () => {
const screen = await render(<QuickCheckoutScreen />);

await fireEvent(
screen.getByRole("switch", { name: "Auto-fill saved information" }),
"valueChange",
true,
);
await fireEvent.changeText(
screen.getByLabelText("Maximum purchase price"),
"60",
);
await fireEvent.press(
screen.getByRole("button", { name: "Simulate Checkout" }),
);
expect(screen.getByText("Checkout simulation successful.")).toBeTruthy();

await fireEvent.changeText(
screen.getByLabelText("Maximum purchase price"),
"40",
);
await fireEvent.press(
screen.getByRole("button", { name: "Simulate Checkout" }),
);
expect(
screen.getByText(
"Checkout simulation stopped: the product exceeds your spending limit.",
),
).toBeTruthy();
});
});
29 changes: 29 additions & 0 deletions apps/mobile/__tests__/recommended-products-screen.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { fireEvent, render } from "@testing-library/react-native";

import { RecommendedProductsScreen } from "../features/recommended-products/RecommendedProductsScreen";

describe("RecommendedProductsScreen", () => {
it("shows clearly labeled sample products with stores and prices", async () => {
const screen = await render(<RecommendedProductsScreen />);

expect(screen.getByText("Recommended Products")).toBeTruthy();
expect(
screen.getByText(/Sample recommendations for preview only/),
).toBeTruthy();
expect(screen.getByText("SteelSeries Arctis Nova 7")).toBeTruthy();
expect(screen.getByText("Amazon")).toBeTruthy();
expect(screen.getByText("$179.99")).toBeTruthy();
});

it("filters recommendations by product or store", async () => {
const screen = await render(<RecommendedProductsScreen />);

await fireEvent.changeText(
screen.getByLabelText("Search recommended products"),
"target",
);

expect(screen.getByText("Pokémon TCG 9-Pocket Binder")).toBeTruthy();
expect(screen.queryByText("SteelSeries Arctis Nova 7")).toBeNull();
});
});
64 changes: 64 additions & 0 deletions apps/mobile/__tests__/watched-items-screen.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { fireEvent, render } from "@testing-library/react-native";

import {
WatchedItemsScreen,
type WatchedItem,
} from "../features/watched-items/WatchedItemsScreen";

describe("WatchedItemsScreen", () => {
it("shows placeholder watched products and lets the user toggle an alert locally", async () => {
const screen = await render(<WatchedItemsScreen />);

expect(screen.getByText("Nintendo Switch 2")).toBeTruthy();
expect(screen.getByText("Target")).toBeTruthy();
expect(screen.getByText("Current price: $449.99")).toBeTruthy();
expect(screen.getByText("Target price: $450.00")).toBeTruthy();

const alertToggle = screen.getByRole("switch", {
name: "Nintendo Switch 2 price alert",
});
expect(alertToggle.props.value).toBe(true);

await fireEvent(alertToggle, "valueChange", false);

expect(
screen.getByRole("switch", { name: "Nintendo Switch 2 price alert" })
.props.value,
).toBe(false);
});

it("adds an item with a target price and removes it locally", async () => {
const screen = await render(<WatchedItemsScreen initialItems={[]} />);

expect(screen.getByText("No watched items yet")).toBeTruthy();

await fireEvent.press(screen.getByRole("button", { name: "Add item" }));
await fireEvent.changeText(
screen.getByLabelText("Product name"),
"LEGO Star Wars Set",
);
await fireEvent.changeText(screen.getByLabelText("Store"), "Amazon");
await fireEvent.changeText(screen.getByLabelText("Current price"), "69.99");
await fireEvent.changeText(screen.getByLabelText("Target price"), "59.99");
await fireEvent.press(screen.getByRole("button", { name: "Watch item" }));

expect(screen.getByText("LEGO Star Wars Set")).toBeTruthy();
expect(screen.getByText("Target price: $59.99")).toBeTruthy();

await fireEvent.press(
screen.getByRole("button", { name: "Unwatch LEGO Star Wars Set" }),
);

expect(screen.getByText("No watched items yet")).toBeTruthy();
});

it("shows an empty state when no watched items are available", async () => {
const items: WatchedItem[] = [];
const screen = await render(<WatchedItemsScreen initialItems={items} />);

expect(screen.getByText("No watched items yet")).toBeTruthy();
expect(
screen.getByText("Add an item to start tracking its price locally."),
).toBeTruthy();
});
});
5 changes: 5 additions & 0 deletions apps/mobile/app/(protected)/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default function HomeRoute() {

return (
<HomeScreen
navigation={{ replace: (route) => router.replace(route as never) }}
onAccountInfo={() => router.push("/(protected)/account")}
onLogout={async () => {
let result = { ok: false };
Expand All @@ -23,6 +24,10 @@ export default function HomeRoute() {
});
}}
onOpenNotifications={() => router.push("/(protected)/notifications")}
onOpenWatchedItems={() => router.push("/(protected)/watched-items")}
onOpenPopularProducts={() =>
router.push("/(protected)/home/popular-products")
}
/>
);
}
18 changes: 18 additions & 0 deletions apps/mobile/app/(protected)/home/popular-products.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useRouter } from "expo-router";

import { PopularProductsScreen } from "../../../features/popular-products/PopularProductsScreen";
import { supabase } from "../../../lib/supabase";

export default function PopularProductsRoute() {
const router = useRouter();

return (
<PopularProductsScreen
navigation={{ replace: (route) => router.replace(route as never) }}
onLogout={async () => {
await supabase.auth.signOut();
router.replace("/");
}}
/>
);
}
Loading
Loading