1
0
mirror of https://github.com/kremalicious/portfolio.git synced 2025-02-03 15:47:31 +01:00

Merge pull request #109 from kremalicious/feature/tests

add more tests
This commit is contained in:
Matthias Kretschmann 2019-04-17 02:39:56 +02:00 committed by GitHub
commit 12f69b4622
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 391 additions and 226 deletions

View File

@ -1,6 +1,6 @@
import './src/styles/global.scss'
import React from 'react'
import AppProvider from './src/store/provider'
import AppProvider from './src/store/Provider'
import wrapPageElementWithTransition from './src/helpers/wrapPageElement'
// IntersectionObserver polyfill for gatsby-image (Safari, IE)

View File

@ -1,6 +1,6 @@
import React from 'react'
import { renderToString } from 'react-dom/server'
import AppProvider from './src/store/provider'
import AppProvider from './src/store/Provider'
import wrapPageElementWithTransition from './src/helpers/wrapPageElement'
export const replaceRenderer = ({ bodyComponent, replaceBodyHTMLString }) => {

View File

@ -9,6 +9,9 @@
"childImageSharp": {
"fluid": {
"src": "/static/b45f45aa8d98d4e4019a242d38f2f248/bc3a8/avatar.jpg"
},
"resize": {
"src": "/static/b45f45aa8d98d4e4019a242d38f2f248/bc3a8/avatar.jpg"
}
}
},

View File

@ -2,3 +2,5 @@ import 'jest-dom/extend-expect'
// this is basically: afterEach(cleanup)
import 'react-testing-library/cleanup-after-each'
import 'jest-canvas-mock'

View File

@ -18,6 +18,7 @@
"format": "prettier --write 'src/**/*.{js,jsx}'",
"format:css": "prettier-stylelint --write --quiet 'src/**/*.{css,scss}'",
"test": "npm run lint && jest --coverage",
"test:watch": "npm run lint && jest --coverage --watch",
"deploy": "./scripts/deploy.sh",
"new": "babel-node ./scripts/new.js"
},
@ -70,6 +71,7 @@
"eslint-plugin-react": "^7.12.4",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.7.1",
"jest-canvas-mock": "^2.0.0-beta.1",
"jest-dom": "^3.1.3",
"ora": "^3.4.0",
"prepend": "^1.0.2",

View File

@ -25,7 +25,11 @@ const query = graphql`
}
`
const LayoutMarkup = ({ children, isHomepage, allowedHosts, location }) => (
const LayoutMarkup = ({ children, data, location }) => {
const { allowedHosts } = data.contentYaml
const isHomepage = location.pathname === '/'
return (
<>
<Typekit />
<HostnameCheck allowedHosts={allowedHosts} />
@ -43,41 +47,40 @@ const LayoutMarkup = ({ children, isHomepage, allowedHosts, location }) => (
<Footer />
</>
)
)
}
LayoutMarkup.propTypes = {
children: PropTypes.any.isRequired,
isHomepage: PropTypes.bool.isRequired,
allowedHosts: PropTypes.array.isRequired,
location: PropTypes.object.isRequired
data: PropTypes.shape({
contentYaml: PropTypes.shape({
allowedHosts: PropTypes.array.isRequired
}).isRequired
}).isRequired,
location: PropTypes.shape({
pathname: PropTypes.string.isRequired
}).isRequired
}
export default class Layout extends PureComponent {
static propTypes = {
children: PropTypes.any.isRequired,
location: PropTypes.object.isRequired
location: PropTypes.shape({
pathname: PropTypes.string.isRequired
}).isRequired
}
render() {
const { children, location } = this.props
const isHomepage = location.pathname === '/'
return (
<StaticQuery
query={query}
render={data => {
const { allowedHosts } = data.contentYaml
return (
<LayoutMarkup
isHomepage={isHomepage}
allowedHosts={allowedHosts}
location={location}
>
render={data => (
<LayoutMarkup data={data} location={location}>
{children}
</LayoutMarkup>
)
}}
)}
/>
)
}

View File

@ -13,7 +13,7 @@ const query = graphql`
email
avatar {
childImageSharp {
original: resize {
resize {
src
}
}
@ -41,7 +41,7 @@ export default class Vcard extends PureComponent {
const handleAddressbookClick = e => {
e.preventDefault()
constructVcard(meta)
init(meta)
}
return (
@ -60,26 +60,32 @@ export default class Vcard extends PureComponent {
}
}
export const init = async meta => {
const photoSrc = meta.avatar.childImageSharp.resize.src
// first, convert the avatar to base64, then construct all vCard elements
const dataUrl = await toDataURL(photoSrc, 'image/jpeg')
const vcard = await constructVcard(dataUrl, meta)
downloadVcard(vcard, meta)
}
// Construct the download from a blob of the just constructed vCard,
// and save it to user's file system
const downloadVcard = (vcard, meta) => {
const name = meta.addressbook.split('/').join('')
export const downloadVcard = (vcard, meta) => {
const { addressbook } = meta
const name = addressbook.split('/').join('')
const blob = new Blob([vcard], { type: 'text/x-vcard' })
saveAs(blob, name)
}
const constructVcard = meta => {
export const constructVcard = async (dataUrl, meta) => {
const contact = new vCard()
const photoSrc = meta.avatar.childImageSharp.original.src
// first, convert the avatar to base64, then construct all vCard elements
toDataURL(
photoSrc,
dataUrl => {
// stripping this data out of base64 string is required
// for vcard to actually display the image for whatever reason
const dataUrlCleaned = dataUrl.split('data:image/jpeg;base64,').join('')
contact.set('photo', dataUrlCleaned, { encoding: 'b', type: 'JPEG' })
// const dataUrlCleaned = dataUrl.split('data:image/jpeg;base64,').join('')
// contact.set('photo', dataUrlCleaned, { encoding: 'b', type: 'JPEG' })
contact.set('fn', meta.title)
contact.set('title', meta.tagline)
contact.set('email', meta.email)
@ -91,36 +97,36 @@ const constructVcard = meta => {
const vcard = contact.toString('3.0')
downloadVcard(vcard, meta)
},
'image/jpeg'
)
return vcard
}
// Helper function to create base64 string from avatar image
// without the need to read image file from file system
const toDataURL = (src, callback, outputFormat) => {
export const toDataURL = async (photoSrc, outputFormat) => {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.src = photoSrc
img.onload = () => {}
img.onload = function() {
// yeah, we're gonna create a fake canvas to render the image
// and then create a base64 string from the rendered result
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let dataURL
canvas.height = this.naturalHeight
canvas.width = this.naturalWidth
ctx.drawImage(this, 0, 0)
canvas.height = img.naturalHeight
canvas.width = img.naturalWidth
ctx.drawImage(img, 0, 0)
dataURL = canvas.toDataURL(outputFormat)
callback(dataURL)
}
img.src = src
if (img.complete || img.complete === undefined) {
img.src =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
img.src = src
}
// img.src = photoSrc
// if (img.complete || img.complete === undefined) {
// img.src =
// 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
// img.src = photoSrc
// }
return dataURL
}

View File

@ -0,0 +1,36 @@
import React from 'react'
import { render } from 'react-testing-library'
import { StaticQuery } from 'gatsby'
import Vcard, { constructVcard, toDataURL, init } from './Vcard'
import data from '../../../jest/__fixtures__/meta.json'
describe('Vcard', () => {
beforeEach(() => {
StaticQuery.mockImplementationOnce(({ render }) => render({ ...data }))
global.URL.createObjectURL = jest.fn()
})
it('renders correctly', () => {
const { container } = render(<Vcard />)
expect(container.firstChild).toBeInTheDocument()
})
it('combined vCard download process finishes', async () => {
await init(data.contentYaml)
expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1)
})
it('vCard can be constructed', async () => {
const vcard = await constructVcard(
'data:image/jpeg;base64,00',
data.contentYaml
)
expect(vcard).toBeDefined()
})
it('Base64 from image can be constructed', async () => {
const dataUrl = await toDataURL('hello', 'image/jpeg')
expect(dataUrl).toBeDefined()
})
})

View File

@ -6,9 +6,7 @@ import data from '../../../jest/__fixtures__/meta.json'
describe('Availability', () => {
it('renders correctly from data file values', () => {
useStaticQuery.mockImplementation(() => {
return { ...data }
})
useStaticQuery.mockImplementation(() => ({ ...data }))
const { container } = render(<Availability />)
expect(container.firstChild).toBeInTheDocument()
})

View File

@ -5,9 +5,7 @@ import Networks from './Networks'
import data from '../../../jest/__fixtures__/meta.json'
beforeEach(() => {
useStaticQuery.mockImplementationOnce(() => {
return { ...data }
})
useStaticQuery.mockImplementationOnce(() => ({ ...data }))
})
describe('Networks', () => {

View File

@ -48,7 +48,10 @@ export default class ThemeSwitch extends PureComponent {
<Helmet>
<body className={dark ? 'dark' : null} />
</Helmet>
<Animation className={styles.themeSwitch}>
<Animation
className={styles.themeSwitch}
data-testid={'theme-switch'}
>
<label className={styles.checkbox}>
<span className={styles.label}>Toggle Night Mode</span>
<ThemeToggleInput dark={dark} toggleDark={toggleDark} />

View File

@ -0,0 +1,17 @@
import React from 'react'
import { render } from 'react-testing-library'
import AppProvider from '../../store/Provider'
import ThemeSwitch from './ThemeSwitch'
describe('ThemeSwitch', () => {
it('renders correctly', () => {
const { getByTestId } = render(
<AppProvider>
<ThemeSwitch />
</AppProvider>
)
expect(getByTestId('theme-switch')).toBeInTheDocument()
expect(getByTestId('theme-switch').nodeName).toBe('ASIDE')
})
})

View File

@ -1,4 +1,5 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { Link, StaticQuery, graphql } from 'gatsby'
import classNames from 'classnames'
import Vcard from '../atoms/Vcard'
@ -10,9 +11,6 @@ const query = graphql`
query {
# the package.json file
portfolioJson {
name
homepage
repository
bugs
}
@ -24,12 +22,10 @@ const query = graphql`
}
`
let classes = classNames('h-card', [styles.footer])
const FooterMarkup = ({ pkg, meta, year }) => {
const classes = classNames('h-card', [styles.footer])
export default class Footer extends PureComponent {
state = { year: new Date().getFullYear() }
FooterMarkup = ({ meta, pkg, year }) => (
return (
<footer className={classes}>
<Link to={'/'}>
<LogoUnit minimal />
@ -55,6 +51,16 @@ export default class Footer extends PureComponent {
</p>
</footer>
)
}
FooterMarkup.propTypes = {
pkg: PropTypes.object.isRequired,
meta: PropTypes.object.isRequired,
year: PropTypes.number.isRequired
}
export default class Footer extends PureComponent {
state = { year: new Date().getFullYear() }
render() {
return (
@ -64,9 +70,7 @@ export default class Footer extends PureComponent {
const pkg = data.portfolioJson
const meta = data.contentYaml
return (
<this.FooterMarkup year={this.state.year} pkg={pkg} meta={meta} />
)
return <FooterMarkup year={this.state.year} pkg={pkg} meta={meta} />
}}
/>
)

View File

@ -0,0 +1,22 @@
import React from 'react'
import { render } from 'react-testing-library'
import { StaticQuery, useStaticQuery } from 'gatsby'
import Footer from './Footer'
import data from '../../../jest/__fixtures__/meta.json'
describe('Header', () => {
beforeEach(() => {
StaticQuery.mockImplementation(({ render }) =>
render({
...data,
portfolioJson: { bugs: '' }
})
)
useStaticQuery.mockImplementation(() => ({ ...data }))
})
it('renders correctly', () => {
const { container } = render(<Footer />)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,22 @@
import React from 'react'
import { render } from 'react-testing-library'
import { StaticQuery, useStaticQuery } from 'gatsby'
import Header from './Header'
import { Provider } from '../../store/createContext'
import data from '../../../jest/__fixtures__/meta.json'
describe('Header', () => {
beforeEach(() => {
StaticQuery.mockImplementation(({ render }) => render({ ...data }))
useStaticQuery.mockImplementation(() => ({ ...data }))
})
it('renders correctly', () => {
const { container } = render(
<Provider value={{ dark: false, toggleDark: () => null }}>
<Header />
</Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -5,9 +5,7 @@ import { useMeta } from './use-meta'
import data from '../../jest/__fixtures__/meta.json'
beforeEach(() => {
useStaticQuery.mockImplementationOnce(() => {
return { ...data }
})
useStaticQuery.mockImplementationOnce(() => ({ ...data }))
})
describe('useMeta', () => {

73
src/store/Provider.jsx Normal file
View File

@ -0,0 +1,73 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { Provider } from './createContext'
import { getLocationTimes } from '../utils/getLocationTimes'
import { getCountry } from '../utils/getCountry'
export default class AppProvider extends PureComponent {
state = {
dark: false,
toggleDark: () => this.toggleDark,
location: null
}
static propTypes = {
children: PropTypes.any.isRequired
}
store = typeof localStorage === 'undefined' ? null : localStorage
mounted = false
async componentDidMount() {
this.mounted = true
const location = await getCountry()
this.setState({ location })
this.checkDark()
}
componentWillUnmount() {
this.mounted = false
}
setDark() {
this.mounted && this.setState({ dark: true })
}
setLight() {
this.mounted && this.setState({ dark: false })
}
darkLocalStorageMode(darkLocalStorage) {
darkLocalStorage === 'true' ? this.setDark() : this.setLight()
}
//
// All the checks to see if we should go dark or light
//
async checkTimes() {
const { location, dark } = this.state
const { sunset, sunrise } = await getLocationTimes(location)
const now = new Date().getHours()
const weWantItDarkTimes = now >= sunset || now <= sunrise
!dark && weWantItDarkTimes ? this.setDark() : this.setLight()
}
async checkDark() {
const darkLocalStorage = await this.store.getItem('dark')
darkLocalStorage
? this.darkLocalStorageMode(darkLocalStorage)
: this.checkTimes()
}
toggleDark = () => {
this.setState({ dark: !this.state.dark })
this.store && this.store.setItem('dark', !this.state.dark)
}
render() {
return <Provider value={this.state}>{this.props.children}</Provider>
}
}

View File

@ -0,0 +1,11 @@
import React from 'react'
import { render } from 'react-testing-library'
import AppProvider from './Provider.jsx'
describe('AppProvider', () => {
it('renders correctly', () => {
const { container } = render(<AppProvider>Hello</AppProvider>)
expect(container.firstChild.textContent).toBe('Hello')
})
})

View File

@ -1,122 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import SunCalc from 'suncalc'
import { Provider } from './createContext'
import countrycodes from './countrycode-latlong.json'
export default class AppProvider extends PureComponent {
state = {
dark: false,
toggleDark: () => this.toggleDark,
location: null
}
static propTypes = {
children: PropTypes.any.isRequired
}
store = typeof localStorage === 'undefined' ? null : localStorage
//
// Get user location from Cloudflare's geo location HTTP header
//
getCountry = async () => {
let trace = []
await fetch('/cdn-cgi/trace?no-cache=1')
.then(data => {
let lines
data.text().then(text => {
lines = text.split('\n')
let keyValue
lines.forEach(line => {
keyValue = line.split('=')
trace[keyValue[0]] = decodeURIComponent(keyValue[1] || '')
if (keyValue[0] === 'loc' && trace['loc'] !== 'XX') {
this.setState({ location: trace['loc'] })
} else {
return
}
})
})
})
.catch(() => null) // fail silently
}
setDark() {
this.setState({ dark: true })
}
setLight() {
this.setState({ dark: false })
}
darkLocalStorageMode(darkLocalStorage) {
if (darkLocalStorage === 'true') {
this.setDark()
} else {
this.setLight()
}
}
//
// All the checks to see if we should go dark or light
//
darkMode() {
const now = new Date().getHours()
const { location } = this.state
// fallback times, in hours
let sunrise = 7
let sunset = 19
// times based on detected country code
if (location && location !== 'XX' && location !== 'T1') {
const country = this.state.location.toLowerCase()
const times = SunCalc.getTimes(
new Date(),
countrycodes[country][0],
countrycodes[country][1]
)
sunrise = times.sunrise.getHours()
sunset = times.sunset.getHours()
}
if (!this.state.dark && (now >= sunset || now <= sunrise)) {
this.setDark()
} else {
this.setLight()
}
}
checkDark() {
const darkLocalStorage = this.store.getItem('dark')
if (darkLocalStorage) {
this.darkLocalStorageMode(darkLocalStorage)
} else {
this.darkMode()
}
}
toggleDark = () => {
this.setState({ dark: !this.state.dark })
if (this.store) {
this.store.setItem('dark', !this.state.dark)
}
}
componentDidMount() {
this.getCountry().then(() => {
this.checkDark()
})
}
render() {
return <Provider value={this.state}>{this.props.children}</Provider>
}
}

28
src/utils/getCountry.js Normal file
View File

@ -0,0 +1,28 @@
//
// Get user location from Cloudflare's geo location HTTP header
//
// @returns country: string
//
export const getCountry = async () => {
try {
const data = await fetch('/cdn-cgi/trace?no-cache=1')
const text = await data.text().replace(/ /g, '')
const lines = text.split('\n')
let keyValue
let trace = []
await lines.forEach(line => {
keyValue = line.split('=')
trace[keyValue[0]] = decodeURIComponent(keyValue[1] || '')
})
const country = trace['loc']
if (country && country !== 'XX') {
return country
}
} catch (error) {
return null // fail silently
}
}

View File

@ -0,0 +1,27 @@
import { getCountry } from './getCountry'
const responseMock = 'loc=DE'
const mockFetch = data =>
jest.fn().mockImplementationOnce(() =>
Promise.resolve({
ok: true,
Id: '123',
text: () => data
// json: () => data
})
)
describe('getCountry', () => {
beforeEach(() => {
global.fetch = mockFetch(responseMock)
})
it('fetches and returns correct value', async () => {
const country = await getCountry()
expect(global.fetch).toHaveBeenCalledTimes(1)
expect(global.fetch).toHaveBeenCalledWith('/cdn-cgi/trace?no-cache=1')
expect(country).toBe('DE')
})
})

View File

@ -0,0 +1,25 @@
import SunCalc from 'suncalc'
import countrycodes from './countrycode-latlong.json'
//
// All the checks to see if we should go dark or light
//
export const getLocationTimes = location => {
// fallback times, in hours
let sunrise = 7
let sunset = 19
// times based on detected country code
if (location && location !== 'XX' && location !== 'T1') {
const country = location.toLowerCase()
const times = SunCalc.getTimes(
new Date(),
countrycodes[country][0],
countrycodes[country][1]
)
sunrise = times.sunrise.getHours()
sunset = times.sunset.getHours()
}
return { sunrise, sunset }
}

View File

@ -0,0 +1,9 @@
import { getLocationTimes } from './getLocationTimes'
describe('getLocationTimes', () => {
it('returns values', async () => {
const { sunset, sunrise } = await getLocationTimes('DE')
expect(sunset).toBeDefined()
expect(sunrise).toBeDefined()
})
})