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
8 changes: 1 addition & 7 deletions __tests__/detail/screens/DetailScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { render, screen } from "@testing-library/react-native";
import { DetailScreen } from "@/src/detail";
import { FavoritesProvider } from "@/src/shared";
import type { Pokemon } from "@/src/shared";

const mockPokemon: Pokemon = {
Expand Down Expand Up @@ -43,12 +42,7 @@ jest.mock("@/src/detail/hooks/usePokemonSpeciesInfo", () => ({
usePokemonSpeciesInfo: () => mockUsePokemonSpeciesInfo,
}));

const renderWithProvider = (id: string) =>
render(
<FavoritesProvider>
<DetailScreen id={id} />
</FavoritesProvider>,
);
const renderWithProvider = (id: string) => render(<DetailScreen id={id} />);

describe("DetailScreen", () => {
beforeEach(() => {
Expand Down
8 changes: 1 addition & 7 deletions __tests__/favorites/screens/FavoritesScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { render, screen } from "@testing-library/react-native";
import { FavoritesScreen } from "@/src/favorites";
import { FavoritesProvider } from "@/src/shared";
import type { PokemonSummary } from "@/src/shared";

const mockUsePokemonByIds = {
Expand All @@ -27,12 +26,7 @@ jest.mock("expo-router", () => ({
},
}));

const renderWithProvider = () =>
render(
<FavoritesProvider>
<FavoritesScreen />
</FavoritesProvider>,
);
const renderWithProvider = () => render(<FavoritesScreen />);

describe("FavoritesScreen", () => {
beforeEach(() => {
Expand Down
8 changes: 1 addition & 7 deletions __tests__/home/screens/HomeScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { render, screen, fireEvent } from "@testing-library/react-native";
import { HomeScreen } from "@/src/home";
import { FavoritesProvider } from "@/src/shared";
import type { PokemonSummary } from "@/src/shared";

const mockPokemon: PokemonSummary[] = [
Expand Down Expand Up @@ -42,12 +41,7 @@ jest.mock("expo-router", () => ({
},
}));

const renderWithProvider = () =>
render(
<FavoritesProvider>
<HomeScreen />
</FavoritesProvider>,
);
const renderWithProvider = () => render(<HomeScreen />);

describe("HomeScreen", () => {
beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { renderHook, act } from "@testing-library/react-native";
import { Alert } from "react-native";
import { FavoritesProvider, useFavorites } from "@/src/shared";
import { useFavorites } from "@/src/shared";
import { useFavoritesStore } from "@/src/shared/stores/useFavoritesStore";

jest.mock("@/src/shared/i18n", () => ({
i18n: {
Expand All @@ -10,31 +11,28 @@ jest.mock("@/src/shared/i18n", () => ({

jest.spyOn(Alert, "alert").mockImplementation(() => {});

describe("FavoritesContext", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<FavoritesProvider>{children}</FavoritesProvider>
);

describe("useFavoritesStore", () => {
beforeEach(() => {
useFavoritesStore.setState({ favoriteIds: [] });
(Alert.alert as jest.Mock).mockClear();
});

describe("useFavorites", () => {
it("初期状態ではお気に入りが空である", () => {
const { result } = renderHook(() => useFavorites(), { wrapper });
const { result } = renderHook(() => useFavorites());
expect(result.current.favoriteIds).toEqual([]);
});

it("toggleFavoriteでポケモンをお気に入りに追加できる", () => {
const { result } = renderHook(() => useFavorites(), { wrapper });
const { result } = renderHook(() => useFavorites());
act(() => {
result.current.toggleFavorite(25);
});
expect(result.current.favoriteIds).toEqual([25]);
});

it("toggleFavoriteで既にお気に入りのポケモンを削除できる", () => {
const { result } = renderHook(() => useFavorites(), { wrapper });
const { result } = renderHook(() => useFavorites());
act(() => {
result.current.toggleFavorite(25);
});
Expand All @@ -45,20 +43,20 @@ describe("FavoritesContext", () => {
});

it("isFavoriteがお気に入り登録済みのポケモンに対してtrueを返す", () => {
const { result } = renderHook(() => useFavorites(), { wrapper });
const { result } = renderHook(() => useFavorites());
act(() => {
result.current.toggleFavorite(25);
});
expect(result.current.isFavorite(25)).toBe(true);
});

it("isFavoriteが未登録のポケモンに対してfalseを返す", () => {
const { result } = renderHook(() => useFavorites(), { wrapper });
const { result } = renderHook(() => useFavorites());
expect(result.current.isFavorite(25)).toBe(false);
});

it("お気に入りが6匹に達している場合、追加できずアラートが表示される", () => {
const { result } = renderHook(() => useFavorites(), { wrapper });
const { result } = renderHook(() => useFavorites());
act(() => {
result.current.toggleFavorite(1);
result.current.toggleFavorite(2);
Expand All @@ -81,7 +79,7 @@ describe("FavoritesContext", () => {
});

it("上限に達していても既存のお気に入りは削除できる", () => {
const { result } = renderHook(() => useFavorites(), { wrapper });
const { result } = renderHook(() => useFavorites());
act(() => {
result.current.toggleFavorite(1);
result.current.toggleFavorite(2);
Expand All @@ -99,7 +97,7 @@ describe("FavoritesContext", () => {
});

it("isFullが上限到達時にtrueを返す", () => {
const { result } = renderHook(() => useFavorites(), { wrapper });
const { result } = renderHook(() => useFavorites());
expect(result.current.isFull).toBe(false);

act(() => {
Expand All @@ -112,13 +110,5 @@ describe("FavoritesContext", () => {
});
expect(result.current.isFull).toBe(true);
});

it("Provider外でuseFavoritesを呼ぶとエラーになる", () => {
jest.spyOn(console, "error").mockImplementation(() => {});
expect(() => {
renderHook(() => useFavorites());
}).toThrow("useFavorites must be used within a FavoritesProvider");
jest.restoreAllMocks();
});
});
});
10 changes: 4 additions & 6 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect } from "react";
import { Stack } from "expo-router";
import { I18nextProvider, useTranslation } from "react-i18next";
import * as SplashScreen from "expo-splash-screen";
import { FavoritesProvider, i18n, initI18n } from "@/src/shared";
import { i18n, initI18n } from "@/src/shared";
import { AnimatedSplash } from "@/src/splash";

SplashScreen.preventAutoHideAsync();
Expand All @@ -25,11 +25,9 @@ function RootLayoutNav() {
export default function RootLayout() {
return (
<I18nextProvider i18n={i18n}>
<FavoritesProvider>
<AnimatedSplash>
<RootLayoutNav />
</AnimatedSplash>
</FavoritesProvider>
<AnimatedSplash>
<RootLayoutNav />
</AnimatedSplash>
</I18nextProvider>
);
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.23.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.7.4"
"react-native-worklets": "0.7.4",
"zustand": "^5.0.12"
},
"expo": {
"install": {
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 0 additions & 58 deletions src/shared/contexts/FavoritesContext.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/shared/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { i18n, initI18n, SUPPORTED_LANGUAGES, STORAGE_KEY } from "./i18n";
export type { SupportedLanguage } from "./i18n";
export { useLanguage } from "./i18n";
export { FavoritesProvider, useFavorites } from "./contexts/FavoritesContext";
export { useFavorites, useFavoritesStore } from "./stores/useFavoritesStore";
export { PokemonCard } from "./components/PokemonCard";
export { FavoriteButton } from "./components/FavoriteButton";
export { typeColors } from "./domain/typeColors";
Expand Down
42 changes: 42 additions & 0 deletions src/shared/stores/useFavoritesStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { create } from "zustand";
import { Alert } from "react-native";
import { i18n } from "../i18n";

const MAX_FAVORITES = 6;

interface FavoritesStoreState {
favoriteIds: number[];
toggleFavorite: (id: number) => void;
}

interface FavoritesValue extends FavoritesStoreState {
isFavorite: (id: number) => boolean;
isFull: boolean;
}

export const useFavoritesStore = create<FavoritesStoreState>((set, get) => ({
favoriteIds: [],

toggleFavorite: (id: number) => {
const { favoriteIds } = get();
if (favoriteIds.includes(id)) {
set({ favoriteIds: favoriteIds.filter((fid) => fid !== id) });
return;
}
if (favoriteIds.length >= MAX_FAVORITES) {
Alert.alert(i18n.t("favorites.limitTitle"), i18n.t("favorites.limitMessage"));
return;
}
set({ favoriteIds: [...favoriteIds, id] });
},
}));

export function useFavorites(): FavoritesValue {
const { favoriteIds, toggleFavorite } = useFavoritesStore();
return {
favoriteIds,
toggleFavorite,
isFavorite: (id: number) => favoriteIds.includes(id),
isFull: favoriteIds.length >= MAX_FAVORITES,
};
}
Loading