Add project
commit
b0006c9027
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||||
|
"env": {
|
||||||
|
"test": {
|
||||||
|
"plugins": ["@babel/plugin-transform-runtime"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
webpack.config.js
|
||||||
|
node_modules/
|
||||||
|
dist/
|
@ -0,0 +1,49 @@
|
|||||||
|
module.exports = {
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"jest/globals": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 2018,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"react", "jest"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix"
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"single"
|
||||||
|
],
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"eqeqeq": "error",
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
|
"object-curly-spacing": [
|
||||||
|
"error", "always"
|
||||||
|
],
|
||||||
|
"arrow-spacing": [
|
||||||
|
"error", { "before": true, "after": true }
|
||||||
|
],
|
||||||
|
"no-console": "error",
|
||||||
|
"react/prop-types": 0
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
node_modules/
|
@ -0,0 +1,15 @@
|
|||||||
|
# Full Stack open CI/CD
|
||||||
|
|
||||||
|
This repository is used for the CI/CD module of the Full stack open course
|
||||||
|
|
||||||
|
Fork the repository to complete course exercises
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Start by running `npm install` inside the project folder
|
||||||
|
|
||||||
|
`npm start` to run the webpack dev server
|
||||||
|
`npm test` to run tests
|
||||||
|
`npm run eslint` to run eslint
|
||||||
|
`npm run build` to make a production build
|
||||||
|
`npm run start-prod` to run your production build
|
@ -0,0 +1,11 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Heroku dynamically sets a port
|
||||||
|
const PORT = process.env.PORT || 5000;
|
||||||
|
|
||||||
|
app.use(express.static("dist"));
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log("server started on port 5000");
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "fullstackopen-cicd",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Full Stack Open",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "webpack-dev-server --open --mode development",
|
||||||
|
"start-prod": "node app.js",
|
||||||
|
"test": "jest",
|
||||||
|
"eslint": "eslint './**/*.{js,jsx}'",
|
||||||
|
"build": "webpack --mode production"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/smartlyio/fullstackopen-cicd.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"fullstack-open"
|
||||||
|
],
|
||||||
|
"author": "Smartly.io",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/smartlyio/fullstackopen-cicd/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/smartlyio/fullstackopen-cicd#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.9.6",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.10.0",
|
||||||
|
"@babel/preset-env": "^7.9.6",
|
||||||
|
"@babel/preset-react": "^7.9.4",
|
||||||
|
"@testing-library/jest-dom": "^5.8.0",
|
||||||
|
"@testing-library/react": "^10.0.4",
|
||||||
|
"babel-jest": "^25.5.1",
|
||||||
|
"babel-loader": "^8.1.0",
|
||||||
|
"css-loader": "^3.5.3",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-plugin-jest": "^23.11.0",
|
||||||
|
"eslint-plugin-react": "^7.19.0",
|
||||||
|
"html-loader": "^1.1.0",
|
||||||
|
"html-webpack-plugin": "^4.3.0",
|
||||||
|
"jest": "^25.5.4",
|
||||||
|
"style-loader": "^1.2.1",
|
||||||
|
"webpack": "^4.43.0",
|
||||||
|
"webpack-cli": "^3.3.11",
|
||||||
|
"webpack-dev-server": "^3.11.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.19.2",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"react-dom": "^16.13.1",
|
||||||
|
"react-router-dom": "^5.2.0"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Pokemon</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<footer>Pokémon and Pokémon character names are trademarks of Nintendo.</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
|
||||||
|
import { useApi } from './useApi'
|
||||||
|
import LoadingSpinner from './LoadingSpinner'
|
||||||
|
import ErrorMessage from './ErrorMessage'
|
||||||
|
import PokemonPage from './PokemonPage'
|
||||||
|
import PokemonList from './PokemonList'
|
||||||
|
|
||||||
|
const mapResults = (({ results }) => results.map(({ url, name }) => ({
|
||||||
|
url,
|
||||||
|
name,
|
||||||
|
id: parseInt(url.match(/\/(\d+)\//)[1])
|
||||||
|
})))
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const { data: pokemonList, error, isLoading } = useApi('https://pokeapi.co/api/v2/pokemon/?limit=784', mapResults)
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingSpinner />
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return <ErrorMessage error={error} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/">
|
||||||
|
<PokemonList pokemonList={pokemonList} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/pokemon/:name" render={(routeParams) => {
|
||||||
|
const pokemonId = pokemonList.find(({ name }) => name === routeParams.match.params.name).id
|
||||||
|
const previous = pokemonList.find(({ id }) => id === pokemonId - 1)
|
||||||
|
const next = pokemonList.find(({ id }) => id === pokemonId + 1)
|
||||||
|
return <PokemonPage pokemonList={pokemonList} previous={previous} next={next} />
|
||||||
|
}} />
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const ErrorMessage = ({ error }) => (
|
||||||
|
<div data-testid="error">An error occured: {error.toString()}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ErrorMessage
|
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const LoadingSpinner = () => (
|
||||||
|
<img className="loading-spinner" alt="Loading..." src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png" />
|
||||||
|
)
|
||||||
|
|
||||||
|
export default LoadingSpinner
|
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const PokemonAbility = ({ abilityName }) => (
|
||||||
|
<div className="pokemon-ability">
|
||||||
|
<div className="pokemon-ability-type">Hidden ability</div>
|
||||||
|
<div className="pokemon-ability-name">
|
||||||
|
{abilityName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default PokemonAbility
|
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
const PokemonList = ({ pokemonList }) => {
|
||||||
|
return (
|
||||||
|
<div className="list-container">
|
||||||
|
{pokemonList.map(({ id, name }) => (
|
||||||
|
<Link key={id} to={`/pokemon/${name}`} className="list-item" style={{ backgroundImage: `url(${`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`})` }}>
|
||||||
|
<div
|
||||||
|
className="list-item-name"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PokemonList
|
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link, useParams } from 'react-router-dom'
|
||||||
|
import LoadingSpinner from './LoadingSpinner'
|
||||||
|
import { useApi } from './useApi'
|
||||||
|
import PokemonAbility from './PokemonAbility'
|
||||||
|
import ErrorMessage from './ErrorMessage'
|
||||||
|
|
||||||
|
const formatName = (nameWithDash) => nameWithDash.replace('-', ' ')
|
||||||
|
|
||||||
|
const PokemonPage = ({ previous, next }) => {
|
||||||
|
const { name } = useParams()
|
||||||
|
const { data: pokemon, error, isLoading } = useApi(`https://pokeapi.co/api/v2/pokemon/${name}`)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingSpinner />
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return <ErrorMessage error={error} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type } = pokemon.types.find((type) => type.slot === 1)
|
||||||
|
const stats = pokemon.stats.map((stat) => ({
|
||||||
|
name: formatName(stat.stat.name),
|
||||||
|
value: stat.base_stat
|
||||||
|
})).reverse()
|
||||||
|
const normalAbility = pokemon.abilities.find((ability) => !ability.is_hidden)
|
||||||
|
const hiddenAbility = pokemon.abilities.find((ability) => ability.is_hidden === true)
|
||||||
|
|
||||||
|
console.log('hiddenAbility=', hiddenAbility)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="links">
|
||||||
|
{previous && <Link to={`/pokemon/${previous.name}`}>Previous</Link>}
|
||||||
|
<Link to="/">Home</Link>
|
||||||
|
{next && <Link to={`/pokemon/${previous.name}`}>Next</Link>}
|
||||||
|
</div>
|
||||||
|
<div className={`pokemon-page pokemon-type-${type.name}`}>
|
||||||
|
<div className="pokemon-image" style={{ backgroundImage: `url(${pokemon.sprites.front_default})` }} />
|
||||||
|
<div className="pokemon-info">
|
||||||
|
<div className="pokemon-name">{pokemon.name}</div>
|
||||||
|
<div className="pokemon-stats" data-testid="stats">
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{stats.map(({ name, value }) => (
|
||||||
|
<tr key={name}>
|
||||||
|
<td className="pokemon-stats-name">{name}</td>
|
||||||
|
<td className="pokemon-stats-value">{value}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="pokemon-abilities">
|
||||||
|
{normalAbility && <PokemonAbility abilityName={formatName(normalAbility.ability.name)} />}
|
||||||
|
{hiddenAbility && <PokemonAbility abilityName={formatName(hiddenAbility.ability.name)} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PokemonPage
|
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import App from './App'
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById('app'))
|
@ -0,0 +1,215 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css?family=Open+Sans|Open+Sans+Condensed:300,700');
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > div {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
animation: rotation 2s infinite linear;
|
||||||
|
margin: 120px;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
@keyframes rotation {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(359deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: #eee;
|
||||||
|
margin: 10px;
|
||||||
|
height: 150px;
|
||||||
|
width: 150px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-name {
|
||||||
|
position: absolute;
|
||||||
|
color: #000;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 332px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-page {
|
||||||
|
background: linear-gradient(140deg, var(--pokemon-type-color) 0%, #ddd 100%);
|
||||||
|
border-radius: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 470px;
|
||||||
|
padding: 16px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-image {
|
||||||
|
min-height: 150px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-name {
|
||||||
|
font-family: "Open Sans Condensed", "Open Sans";
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-info {
|
||||||
|
background-color: rgba(255, 255, 255, 0.65);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-stats {
|
||||||
|
flex: 1;
|
||||||
|
font-family: "Open Sans Condensed", "Open Sans";
|
||||||
|
padding-top: 15px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-stats table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-stats-name {
|
||||||
|
font-weight: 300;
|
||||||
|
padding: 4px 8px 0 8px;
|
||||||
|
text-align: right;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-stats-value {
|
||||||
|
padding: 4px 8px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-abilities {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 20px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-ability {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-ability-type {
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-bug {
|
||||||
|
--pokemon-type-color: #ac2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-dark {
|
||||||
|
--pokemon-type-color: #754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-dragon {
|
||||||
|
--pokemon-type-color: #73f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-electric {
|
||||||
|
--pokemon-type-color: #fd3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-fairy {
|
||||||
|
--pokemon-type-color: #d8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-fighting {
|
||||||
|
--pokemon-type-color: #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-fire {
|
||||||
|
--pokemon-type-color: #f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-flying {
|
||||||
|
--pokemon-type-color: #b9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-ghost {
|
||||||
|
--pokemon-type-color: #759;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-grass {
|
||||||
|
--pokemon-type-color: #8c5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-ground {
|
||||||
|
--pokemon-type-color: #ec6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-ice {
|
||||||
|
--pokemon-type-color: #9ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-normal {
|
||||||
|
--pokemon-type-color: #ba8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-poison {
|
||||||
|
--pokemon-type-color: #a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-psychic {
|
||||||
|
--pokemon-type-color: #f59;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-rock {
|
||||||
|
--pokemon-type-color: #ba3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-steel {
|
||||||
|
--pokemon-type-color: #bcd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-type-water {
|
||||||
|
--pokemon-type-color: #69f;
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const useApi = (url, mapResults = (result) => result) => {
|
||||||
|
const [data, setData] = useState()
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [error, setError] = useState()
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.get(url)
|
||||||
|
.then(response => setData(mapResults(response.data)))
|
||||||
|
.catch(setError)
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}, [url])
|
||||||
|
|
||||||
|
return { data, isLoading, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useApi }
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import axiosMock from 'axios'
|
||||||
|
import { act } from 'react-dom/test-utils'
|
||||||
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
|
import App from '../src/App'
|
||||||
|
|
||||||
|
jest.mock('axios')
|
||||||
|
|
||||||
|
describe('<App />', () => {
|
||||||
|
it('fetches data', async () => {
|
||||||
|
axiosMock.get.mockResolvedValueOnce(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
results: [{ url: 'https://pokeapi.co/api/v2/pokemon/1/', name: 'bulbasaur', id: 1 }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await act(async () => {
|
||||||
|
render(<App />)
|
||||||
|
})
|
||||||
|
expect(axiosMock.get).toHaveBeenCalledTimes(1)
|
||||||
|
expect(axiosMock.get).toHaveBeenCalledWith('https://pokeapi.co/api/v2/pokemon/?limit=784')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows LoadingSpinner', async () => {
|
||||||
|
axiosMock.get.mockResolvedValueOnce({})
|
||||||
|
await act(async () => {
|
||||||
|
const { getByAltText } = render(<App />)
|
||||||
|
expect(getByAltText('Loading...')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error', async () => {
|
||||||
|
axiosMock.get.mockRejectedValueOnce(new Error())
|
||||||
|
await act(async () => {
|
||||||
|
render(<App />)
|
||||||
|
})
|
||||||
|
expect(screen.getByTestId('error')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
|
import PokemonList from '../src/PokemonList'
|
||||||
|
|
||||||
|
|
||||||
|
const pokemonList = [{
|
||||||
|
url: 'https://pokeapi.co/api/v2/pokemon/1/',
|
||||||
|
name: 'bulbasaur',
|
||||||
|
id: 1
|
||||||
|
}, {
|
||||||
|
url: 'https://pokeapi.co/api/v2/pokemon/133/',
|
||||||
|
name: 'eevee',
|
||||||
|
id: 133
|
||||||
|
}]
|
||||||
|
|
||||||
|
describe('<PokemonList />', () => {
|
||||||
|
it('should render items', () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<PokemonList pokemonList={pokemonList} />
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
expect(screen.getByText('bulbasaur')).toBeVisible()
|
||||||
|
expect(screen.getByText('eevee')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,139 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { Router } from 'react-router-dom'
|
||||||
|
import { createMemoryHistory } from 'history'
|
||||||
|
import axiosMock from 'axios'
|
||||||
|
import { act } from 'react-dom/test-utils'
|
||||||
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
|
import PokemonPage from '../src/PokemonPage'
|
||||||
|
|
||||||
|
jest.mock('axios')
|
||||||
|
|
||||||
|
const previous = {
|
||||||
|
url: 'https://pokeapi.co/api/v2/pokemon/132/',
|
||||||
|
name: 'ditto',
|
||||||
|
id: 132
|
||||||
|
}
|
||||||
|
const next = {
|
||||||
|
url: 'https://pokeapi.co/api/v2/pokemon/134/',
|
||||||
|
name: 'vaporeon',
|
||||||
|
id: 134
|
||||||
|
}
|
||||||
|
|
||||||
|
const pokemonList = {
|
||||||
|
id: 133,
|
||||||
|
abilities: [
|
||||||
|
{
|
||||||
|
ability: {
|
||||||
|
name: 'anticipation',
|
||||||
|
url: 'https://pokeapi.co/api/v2/ability/107/'
|
||||||
|
},
|
||||||
|
is_hidden: true,
|
||||||
|
slot: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ability: {
|
||||||
|
name: 'adaptability',
|
||||||
|
url: 'https://pokeapi.co/api/v2/ability/91/'
|
||||||
|
},
|
||||||
|
is_hidden: false,
|
||||||
|
slot: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
name: 'eevee',
|
||||||
|
stats: [
|
||||||
|
{
|
||||||
|
base_stat: 55,
|
||||||
|
effort: 0,
|
||||||
|
stat: {
|
||||||
|
name: 'attack',
|
||||||
|
url: 'https://pokeapi.co/api/v2/stat/2/'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base_stat: 55,
|
||||||
|
effort: 0,
|
||||||
|
stat: {
|
||||||
|
name: 'hp',
|
||||||
|
url: 'https://pokeapi.co/api/v2/stat/1/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
types: [
|
||||||
|
{
|
||||||
|
slot: 1,
|
||||||
|
type: {
|
||||||
|
name: 'normal',
|
||||||
|
url: 'https://pokeapi.co/api/v2/type/1/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
sprites: { front_default: 'URL' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = createMemoryHistory()
|
||||||
|
|
||||||
|
describe('<PokemonPage />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
history.push('/pokemon/eevee')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render abilities', async () => {
|
||||||
|
axiosMock.get.mockResolvedValueOnce({ data: pokemonList })
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<Router history={history}>
|
||||||
|
<PokemonPage />
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByText('adaptability')).toBeVisible()
|
||||||
|
expect(screen.getByText('anticipation')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render stats', async () => {
|
||||||
|
axiosMock.get.mockResolvedValueOnce({ data: pokemonList })
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<Router history={history}>
|
||||||
|
<PokemonPage />
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByTestId('stats')).toHaveTextContent('hp55attack55')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render previous and next urls if they exist', async () => {
|
||||||
|
axiosMock.get.mockResolvedValueOnce({ data: pokemonList })
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<Router history={history}>
|
||||||
|
<PokemonPage previous={previous} next={next} />
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByText('Previous')).toHaveAttribute('href', '/pokemon/ditto')
|
||||||
|
expect(screen.getByText('Next')).toHaveAttribute('href', '/pokemon/vaporeon')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not render previous and next urls if none exist', async () => {
|
||||||
|
axiosMock.get.mockResolvedValueOnce({ data: pokemonList })
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<Router history={history}>
|
||||||
|
<PokemonPage />
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.queryByText('Previous')).toBeNull()
|
||||||
|
expect(screen.queryByText('Next')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,46 @@
|
|||||||
|
const HtmlWebPackPlugin = require("html-webpack-plugin");
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: "./src/index.jsx",
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'bundle.js',
|
||||||
|
publicPath: '/'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.html$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: "html-loader",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
loaders: ['style-loader', 'css-loader'],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ["*", ".js", ".jsx"],
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebPackPlugin({
|
||||||
|
template: "./public/index.html",
|
||||||
|
filename: "./index.html",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
Loading…
Reference in New Issue