Compare commits

...

3 Commits

15 changed files with 10749 additions and 70 deletions

View File

@ -0,0 +1,81 @@
{
"baby_trigger_item": null,
"chain": {
"evolution_details": [],
"evolves_to": [
{
"evolution_details": [
{
"gender": null,
"held_item": null,
"item": null,
"known_move": null,
"known_move_type": null,
"location": null,
"min_affection": null,
"min_beauty": null,
"min_happiness": null,
"min_level": 16,
"needs_overworld_rain": false,
"party_species": null,
"party_type": null,
"relative_physical_stats": null,
"time_of_day": "",
"trade_species": null,
"trigger": {
"name": "level-up",
"url": "https://pokeapi.co/api/v2/evolution-trigger/1/"
},
"turn_upside_down": false
}
],
"evolves_to": [
{
"evolution_details": [
{
"gender": null,
"held_item": null,
"item": null,
"known_move": null,
"known_move_type": null,
"location": null,
"min_affection": null,
"min_beauty": null,
"min_happiness": null,
"min_level": 36,
"needs_overworld_rain": false,
"party_species": null,
"party_type": null,
"relative_physical_stats": null,
"time_of_day": "",
"trade_species": null,
"trigger": {
"name": "level-up",
"url": "https://pokeapi.co/api/v2/evolution-trigger/1/"
},
"turn_upside_down": false
}
],
"evolves_to": [],
"is_baby": false,
"species": {
"name": "charizard",
"url": "https://pokeapi.co/api/v2/pokemon-species/6/"
}
}
],
"is_baby": false,
"species": {
"name": "charmeleon",
"url": "https://pokeapi.co/api/v2/pokemon-species/5/"
}
}
],
"is_baby": false,
"species": {
"name": "charmander",
"url": "https://pokeapi.co/api/v2/pokemon-species/4/"
}
},
"id": 2
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,90 @@
import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
import { pokeApi } from 'app/services/pokeApi';
import { filterSlice } from '../../features/Filters/filterSlice';
import { configureStore } from '@reduxjs/toolkit';
import { AppStore } from 'app/store';
import {
EvolutionChainResponseData,
PokemonResponseData,
PokemonSpeciesResponseData,
TypeListResponseData,
} from 'types/api';
let store: AppStore;
describe('pokeApi', () => {
beforeEach(() => {
store = configureStore({
reducer: {
pokedex: pokedexSlice.reducer,
filter: filterSlice.reducer,
[pokeApi.reducerPath]: pokeApi.reducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(pokeApi.middleware),
});
});
describe('JEST test against mock API', () => {
describe('test getPokemon query', () => {
test('visit https://pokeapi.co/api/v2/pokemon/85 returns Dodrio', async () => {
await store.dispatch(pokeApi.endpoints.getPokemon.initiate(85));
const pokemon = pokeApi.endpoints.getPokemon.select(85)(
store.getState(),
).data as PokemonResponseData;
expect(pokemon?.name).toBe('dodrio');
expect(pokemon?.types).toHaveLength(2);
expect(pokemon?.types[0].type.name).toBe('normal');
expect(pokemon?.types[1].type.name).toBe('flying');
expect(pokemon?.sprites.other.dream_world.front_default).toBe(
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/85.svg',
);
});
});
describe('test getPokemonSpecies query', () => {
test('visit https://pokeapi.co/api/v2/pokemon-species/6/', async () => {
await store.dispatch(pokeApi.endpoints.getPokemonSpecies.initiate(6));
const pokemonSpecies = pokeApi.endpoints.getPokemonSpecies.select(6)(
store.getState(),
).data as PokemonSpeciesResponseData;
expect(pokemonSpecies?.evolution_chain.url).toBe(
'https://pokeapi.co/api/v2/evolution-chain/2/',
);
});
});
describe('test getTypeList query', () => {
test('visit https://pokeapi.co/api/v2/type should return correct data in list', async () => {
await store.dispatch(pokeApi.endpoints.getTypeList.initiate());
const typeListData = pokeApi.endpoints.getTypeList.select()(
store.getState(),
).data as TypeListResponseData;
expect(typeListData?.results).toHaveLength(typeListData.count + 1);
});
});
describe('test getEvolutionChain query', () => {
test('visit https://pokeapi.co/api/v2/evolution-chain/2/', async () => {
await store.dispatch(pokeApi.endpoints.getEvolutionChain.initiate(2));
const evolutionChainData = pokeApi.endpoints.getEvolutionChain.select(
2,
)(store.getState()).data as EvolutionChainResponseData;
expect(evolutionChainData?.chain.species.url).toBe(
'https://pokeapi.co/api/v2/pokemon-species/4/',
);
expect(evolutionChainData?.chain.evolves_to[0].species.url).toBe(
'https://pokeapi.co/api/v2/pokemon-species/5/',
);
expect(
evolutionChainData?.chain.evolves_to[0].evolves_to[0].species.url,
).toBe('https://pokeapi.co/api/v2/pokemon-species/6/');
});
});
});
});

View File

@ -1,4 +1,15 @@
import { fetchBaseQuery, FetchArgs } from '@reduxjs/toolkit/query/react'; import {
fetchBaseQuery,
FetchArgs,
createApi,
} from '@reduxjs/toolkit/query/react';
import {
RegionListResponseData,
TypeListResponseData,
PokemonResponseData,
EvolutionChainResponseData,
PokemonSpeciesResponseData,
} from 'types/api';
export interface pokeApiFullListFetchArgs extends FetchArgs { export interface pokeApiFullListFetchArgs extends FetchArgs {
fetchAllPages?: boolean; fetchAllPages?: boolean;
@ -69,3 +80,38 @@ export const pokeApiBaseQuery = async (
return fetchBaseQuery({ baseUrl })(args, api, extra); return fetchBaseQuery({ baseUrl })(args, api, extra);
} }
}; };
export const pokeApi = createApi({
reducerPath: 'pokeApi',
baseQuery: pokeApiBaseQuery,
endpoints: builder => ({
getTypeList: builder.query<TypeListResponseData, void>({
query: () => ({ url: 'type', fetchAllPages: true }),
transformResponse: (response: RegionListResponseData) => {
return {
...response,
results: [{ name: 'All Types', url: '' }, ...response.results],
};
},
}),
getPokemon: builder.query<PokemonResponseData, number | string>({
query: IdOrName => ({ url: `pokemon/${IdOrName}` }),
}),
getPokemonSpecies: builder.query<
PokemonSpeciesResponseData,
number | string
>({
query: IdOrName => ({ url: `pokemon-species/${IdOrName}` }),
}),
getEvolutionChain: builder.query<EvolutionChainResponseData, number>({
query: Id => ({ url: `evolution-chain/${Id}` }),
}),
}),
});
export const {
useGetTypeListQuery,
useGetPokemonQuery,
useGetPokemonSpeciesQuery,
useGetEvolutionChainQuery,
} = pokeApi;

View File

@ -1,7 +1,7 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import { pokedexSlice } from 'features/Pokedex/pokedexSlice'; import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
import { filterSlice } from 'features/Filters/filterSlice'; import { filterSlice } from 'features/Filters/filterSlice';
import { filterApi } from 'features/Filters/filterApi'; import { pokeApi } from './services/pokeApi';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
@ -10,10 +10,10 @@ export const store = configureStore({
filter: filterSlice.reducer, filter: filterSlice.reducer,
// api slices // api slices
[filterApi.reducerPath]: filterApi.reducer, [pokeApi.reducerPath]: pokeApi.reducer,
}, },
middleware: getDefaultMiddleware => middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(filterApi.middleware), getDefaultMiddleware().concat(pokeApi.middleware),
devTools: true, devTools: true,
}); });

View File

@ -11,7 +11,7 @@ import {
setSortOptions, setSortOptions,
setSearchInput, setSearchInput,
} from './filterSlice'; } from './filterSlice';
import { useGetTypeListQuery } from './filterApi'; import { useGetTypeListQuery } from 'app/services/pokeApi';
import { RegionPokemonRange } from './types/slice'; import { RegionPokemonRange } from './types/slice';
import './Filters.css'; import './Filters.css';

View File

@ -1,34 +0,0 @@
import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
import { filterApi } from './filterApi';
import { filterSlice } from './filterSlice';
import { configureStore } from '@reduxjs/toolkit';
import { AppStore } from 'app/store';
import { TypeListResponseData } from 'types/api';
let store: AppStore;
describe('filterApi', () => {
beforeEach(() => {
store = configureStore({
reducer: {
pokedex: pokedexSlice.reducer,
filter: filterSlice.reducer,
[filterApi.reducerPath]: filterApi.reducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(filterApi.middleware),
});
});
describe('JEST test against mock API', () => {
test('visit https://pokeapi.co/api/v2/type should return correct data in list', async () => {
await store.dispatch(filterApi.endpoints.getTypeList.initiate());
const typeListData = filterApi.endpoints.getTypeList.select()(
store.getState(),
).data as TypeListResponseData;
expect(typeListData?.results).toHaveLength(typeListData.count + 1);
});
});
});

View File

@ -1,21 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { pokeApiBaseQuery } from '../../services/pokeapi/paginationBaseQuery';
import { RegionListResponseData, TypeListResponseData } from 'types/api';
export const filterApi = createApi({
reducerPath: 'filterApi',
baseQuery: pokeApiBaseQuery,
endpoints: builder => ({
getTypeList: builder.query<TypeListResponseData, void>({
query: () => ({ url: 'type', fetchAllPages: true }),
transformResponse: (response: RegionListResponseData) => {
return {
...response,
results: [{ name: 'All Types', url: '' }, ...response.results],
};
},
}),
}),
});
export const { useGetTypeListQuery } = filterApi;

View File

@ -1,9 +1,9 @@
import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit'; import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit';
import { FilterState } from './types/slice'; import { FilterState } from './types/slice';
import { RegionPokemonRange } from './types/slice'; import { RegionPokemonRange } from './types/slice';
import { filterApi } from './filterApi'; import { pokeApi } from 'app/services/pokeApi';
filterApi.endpoints.getTypeList.initiate(); // initialize type list fetching pokeApi.endpoints.getTypeList.initiate(); // initialize type list fetching
const initialState: FilterState = { const initialState: FilterState = {
regionOptions: [], regionOptions: [],
@ -46,7 +46,7 @@ export const filterSlice: Slice<FilterState> = createSlice({
}, },
extraReducers: builder => { extraReducers: builder => {
builder.addMatcher( builder.addMatcher(
filterApi.endpoints.getTypeList.matchFulfilled, pokeApi.endpoints.getTypeList.matchFulfilled,
(state, action) => { (state, action) => {
if (action.payload && action.payload.results.length > 0) { if (action.payload && action.payload.results.length > 0) {
const regionListResults = action.payload.results; const regionListResults = action.payload.results;

View File

@ -10,7 +10,7 @@ import { AppStore } from 'app/store';
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import { pokedexSlice } from 'features/Pokedex/pokedexSlice'; import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
import { filterSlice } from 'features/Filters/filterSlice'; import { filterSlice } from 'features/Filters/filterSlice';
import { filterApi } from 'features/Filters/filterApi'; import { pokeApi } from 'app/services/pokeApi';
let store: AppStore; let store: AppStore;
@ -20,10 +20,10 @@ describe('pokedex Component', () => {
reducer: { reducer: {
pokedex: pokedexSlice.reducer, pokedex: pokedexSlice.reducer,
filter: filterSlice.reducer, filter: filterSlice.reducer,
[filterApi.reducerPath]: filterApi.reducer, [pokeApi.reducerPath]: pokeApi.reducer,
}, },
middleware: getDefaultMiddleware => middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(filterApi.middleware), getDefaultMiddleware().concat(pokeApi.middleware),
}); });
}); });

View File

@ -1,7 +1,7 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import type { Slice, PayloadAction } from '@reduxjs/toolkit'; import type { Slice, PayloadAction } from '@reduxjs/toolkit';
import { PokedexState } from 'features/Pokedex/types/slice'; import { PokedexStateProps } from 'features/Pokedex/types/slice';
import { getStartAndEndIdsForRegion } from './utils'; import { getStartAndEndIdsForRegion } from './utils';
import { PokemonResponseData } from 'types/api'; import { PokemonResponseData } from 'types/api';
@ -39,12 +39,12 @@ export const fetchPokemonsInTheRegion = createAsyncThunk<
return pokemonListData; return pokemonListData;
}); });
const initialState: PokedexState = { export const initialState: PokedexStateProps = {
isLoadingPokemons: true, isLoadingPokemons: true,
pokemonCardList: [], pokemonCardList: [],
}; };
export const pokedexSlice: Slice<PokedexState> = createSlice({ export const pokedexSlice: Slice<PokedexStateProps> = createSlice({
name: 'pokedex', name: 'pokedex',
initialState, initialState,
reducers: { reducers: {

View File

@ -1,6 +1,6 @@
import { PokemonCardProps } from 'components/PokemonCard'; import { PokemonCardProps } from 'components/PokemonCard';
export type PokedexState = { export type PokedexStateProps = {
isLoadingPokemons: boolean; isLoadingPokemons: boolean;
pokemonCardList: PokemonCardProps[]; pokemonCardList: PokemonCardProps[];
}; };

View File

@ -6,6 +6,10 @@ import typeList from 'features/Filters/__test__/responses/typeList.json';
import pokemonListPg1 from 'features/Pokedex/__test__/responses/pokemonListPage1.json'; import pokemonListPg1 from 'features/Pokedex/__test__/responses/pokemonListPage1.json';
import pokemonListPg2 from 'features/Pokedex/__test__/responses/pokemonListPage2.json'; import pokemonListPg2 from 'features/Pokedex/__test__/responses/pokemonListPage2.json';
import dodrio from 'app/services/__test__/responses/pokemon_85.json';
import pokemonSpecies6 from 'app/services/__test__/responses/pokemon-species_6.json';
import evolutionChain2 from 'app/services/__test__/responses/evolution-chain_2.json';
export const handlers = [ export const handlers = [
// mock https://pokeapi.co/api/v2/region/1 // mock https://pokeapi.co/api/v2/region/1
rest.get('https://pokeapi.co/api/v2/region/999999', (req, res, ctx) => { rest.get('https://pokeapi.co/api/v2/region/999999', (req, res, ctx) => {
@ -34,4 +38,19 @@ export const handlers = [
return res(ctx.json(pokemonListPg2)); return res(ctx.json(pokemonListPg2));
} }
}), }),
// mock https://pokeapi.co/api/v2/pokemon/85
rest.get('https://pokeapi.co/api/v2/pokemon/85', (req, res, ctx) => {
return res(ctx.json(dodrio));
}),
// mock https://pokeapi.co/api/v2/pokemon-species/6
rest.get('https://pokeapi.co/api/v2/pokemon-species/6', (req, res, ctx) => {
return res(ctx.json(pokemonSpecies6));
}),
// mock https://pokeapi.co/api/v2/evolution-chain/2
rest.get('https://pokeapi.co/api/v2/evolution-chain/2', (req, res, ctx) => {
return res(ctx.json(evolutionChain2));
}),
]; ];

View File

@ -60,3 +60,23 @@ export interface PokemonResponseData {
}; };
}; };
} }
export type PokemonSpeciesResponseData = {
// many fields are ignored
evolution_chain: {
url: string;
};
};
type EvolutionChain = {
evolves_to: EvolutionChain[];
species: {
name: string;
url: string;
};
};
export type EvolutionChainResponseData = {
// many fields are ignored
chain: EvolutionChain;
};