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