Skip to content
This repository has been archived by the owner on Nov 26, 2020. It is now read-only.

Commit

Permalink
improve cypress-client integration and do more things the cypress way
Browse files Browse the repository at this point in the history
  • Loading branch information
Kent C. Dodds committed Feb 24, 2018
1 parent 02c79ec commit 93fe350
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 124 deletions.
3 changes: 1 addition & 2 deletions .as-a.ini
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
[E2E]
CLIENT_PORT=8001
CYPRESS_CLIENT_URL=http://localhost:8001
SERVER_PORT=3001
CYPRESS_API_URL=http://localhost:3001/api
REACT_APP_API_URL=http://localhost:3001/api
BROWSER=none
CYPRESS_baseUrl=http://localhost:8001

[DEV]
CLIENT_PORT=3000
Expand Down
11 changes: 7 additions & 4 deletions client/src/__tests__/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import {init as initAPI} from '../utils/api'
import App from '../app'

beforeEach(() => {
window.localStorage.removeItem('jwt')
window.localStorage.removeItem('token')
axiosMock.__mock.reset()
initAPI()
})

test('register a new user', async () => {
Expand Down Expand Up @@ -60,7 +61,7 @@ test('register a new user', async () => {
wrapper.update()

// assert the state of the world
expect(window.localStorage.getItem('jwt')).toBe(token)
expect(window.localStorage.getItem('token')).toBe(token)
expect(window.location.href).not.toContain('register')
expect(findNodeByTestId('username-display').text()).toEqual(fakeUser.username)
expect(findNodeByTestId('logout-button').length).toBe(1)
Expand Down Expand Up @@ -108,7 +109,7 @@ test('login', async () => {
wrapper.update()

// assert the state of the world
expect(window.localStorage.getItem('jwt')).toBe(token)
expect(window.localStorage.getItem('token')).toBe(token)
expect(window.location.href).not.toContain('login')
expect(findNodeByTestId('username-display').text()).toEqual(fakeUser.username)
expect(findNodeByTestId('logout-button').length).toBe(1)
Expand All @@ -117,11 +118,13 @@ test('login', async () => {
test('create post', async () => {
// setup things to simulate being logged in
const {get, post} = axiosMock.__mock.instance
window.localStorage.setItem('jwt', 'my.fake.jwt')
const fakeToken = 'my.fake.token'
window.localStorage.setItem('token', fakeToken)
initAPI()
const fakeUser = {
username: generate.username(),
id: generate.id(),
token: fakeToken,
}
get.mockImplementation(url => {
if (url === '/auth/me') {
Expand Down
6 changes: 5 additions & 1 deletion client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {init} from './utils/api'

import './index.css'

init()
if (window.Cypress) {
init({baseURL: window.Cypress.env('API_URL').trim()})
} else {
init()
}

ReactDOM.render(<App />, document.getElementById('⚛️'))
52 changes: 29 additions & 23 deletions client/src/utils/api.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import axios from 'axios'
import queryString from 'query-string'

const api = axios.create({
baseURL:
queryString.parse(window.location.search)['api-url'] ||
process.env.REACT_APP_API_URL,
})
let api, isLoggedIn

const getData = res => res.data

Expand All @@ -18,32 +13,29 @@ const requests = {

const auth = {
me() {
if (!api.defaults.headers.common.authorization) {
if (!isLoggedIn) {
return Promise.resolve({user: null})
}
return requests.get('/auth/me').catch(error => {
if (error.response.status === 401) {
window.localStorage.removeItem('jwt')
setToken(null)
logout()
return {user: null}
}
return Promise.reject(error)
})
},
logout: () => {
setToken(null)
logout()
return Promise.resolve({user: null})
},
login: form =>
requests.post('/auth/login', form).then(data => {
window.localStorage.setItem('jwt', data.user.token)
setToken(data.user.token)
login({token: data.user.token})
return data
}),
register: form =>
requests.post('/auth/register', form).then(data => {
window.localStorage.setItem('jwt', data.user.token)
setToken(data.user.token)
login({token: data.user.token})
return data
}),
}
Expand All @@ -62,16 +54,30 @@ const posts = {
create: post => requests.post('/posts', post),
}

function setToken(token) {
if (token) {
api.defaults.headers.common.authorization = `Bearer ${token}`
} else {
delete api.defaults.headers.common.authorization
}
function logout() {
window.localStorage.removeItem('token')
init({token: null})
}

function init() {
setToken(window.localStorage.getItem('jwt'))
function login({token}) {
window.localStorage.setItem('token', token)
init({token})
}

export {init, users, posts, auth, setToken}
function init({
token = window.localStorage.getItem('token'),
baseURL = (api && api.defaults.baseUrl) || process.env.REACT_APP_API_URL,
axiosOptions = {headers: {}},
} = {}) {
isLoggedIn = Boolean(token)
api = axios.create({
baseURL,
...axiosOptions,
headers: {
authorization: token ? `Bearer ${token}` : undefined,
...axiosOptions.headers,
},
})
}

export {init, users, posts, auth}
34 changes: 17 additions & 17 deletions cypress/e2e/auth.login.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@
// Normally I'd just have a file called `auth` and have all my tests
// in that file. But I've split them up like this to make the workshop
// flow nicer with the demo and exercises.
import {createNewUser, logout, assertRoute} from '../utils'

describe('authentication', () => {
let user
beforeEach(() => {
return logout()
return cy
.logout()
.createNewUser()
.then(u => (user = u))
.visit('/')
})

it('should allow existing users to login', () => {
createNewUser().then(user => {
cy
.visitApp()
.getByTestId('login-link')
.click()
.getByTestId('username-input')
.type(user.username)
.getByTestId('password-input')
.type(user.password)
.getByTestId('login-submit')
.click()
assertRoute('/')
cy.getByTestId('username-display').should('contain', user.username)
})
cy
.getByTestId('login-link')
.click()
.getByTestId('username-input')
.type(user.username)
.getByTestId('password-input')
.type(user.password)
.getByTestId('login-submit')
.click()
.assertRoute('/')
cy.getByTestId('username-display').should('contain', user.username)
})
})
25 changes: 20 additions & 5 deletions cypress/e2e/auth.login.todo.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
// in that file. But I've split them up like this to make the workshop
// flow nicer with the demo and exercises.
// eslint-disable-next-line
import {createNewUser, logout, assertRoute} from '../utils'
import {generate} from '../utils'

describe('authentication', () => {
beforeEach(() => logout())
beforeEach(() => cy.logout())

it('should allow existing users to login', () => {
// you'll want to first create a new user.
// This returns a promise, so you can do:
// createNewUser().then(user => {
// cy commands
// This custom cypress command is similar to a promise, so you can do:
// cy.createNewUser().then(user => {
// more cy commands here
// })
//
// With the user created, go ahead and use the cy commands to:
Expand All @@ -25,4 +25,19 @@ describe('authentication', () => {
// Finally assert the route changed to '/'
// and verify that the display name contains user.username
})

//////// Elaboration & Feedback /////////
// When you've finished with the exercises:
// 1. Copy the URL below into your browser and fill out the form
// 2. remove the `.skip` from the test below
// 3. Change submitted from `false` to `true`
// 4. And you're all done!
/*
http://ws.kcd.im/?ws=Testing&e=e2e%20register&em=
*/
it.skip('I submitted my elaboration and feedback', () => {
const submitted = false // change this when you've submitted!
expect(submitted).toBe(true)
})
////////////////////////////////
})
8 changes: 5 additions & 3 deletions cypress/e2e/auth.register.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// Normally I'd just have a file called `auth` and have all my tests
// in that file. But I've split them up like this to make the workshop
// flow nicer with the demo and exercises.
import {generate, assertRoute} from '../utils'
import {generate} from '../utils'

describe('authentication', () => {
beforeEach(() => {
return cy.logout().visit('/')
})
it('should allow users to register', () => {
const user = generate.loginForm()
cy
.visitApp()
.getByTestId('register-link')
.click()
.getByTestId('username-input')
Expand All @@ -17,7 +19,7 @@ describe('authentication', () => {
.type(user.password)
.getByTestId('login-submit')
.click()
assertRoute('/')
.assertRoute('/')
cy.getByTestId('username-display').should('contain', user.username)
})
})
71 changes: 36 additions & 35 deletions cypress/e2e/posts.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,49 @@
import {loginAsNewUser, logout, generate, assertRoute} from '../utils'
import {generate} from '../utils'

describe('authentication', () => {
describe('posts', () => {
let user
beforeEach(() => {
return logout()
return cy
.loginAsNewUser()
.then(u => (user = u))
.visit('/')
})

it('should allow existing users to login', () => {
const fakePost = generate.postData()
// shorten the content so we don't have to wait so long
fakePost.content = fakePost.content.slice(0, 50)
loginAsNewUser().then(user => {
cy
.getByTestId('create-post-link')
.click()
.getByTestId('title-input')
.type(fakePost.title)
.getByTestId('content-input')
// the delay is because the content takes
// forever to type otherwise
.type(fakePost.content, {delay: 1})
.getByTestId('tags-input')
.type(fakePost.tags.join(', '))
.getByTestId('editor-submit')
.click()
.assertRoute('/')
cy
.getByTestId('post-title')
.first()
.should('contain', fakePost.title)
cy
.getByTestId('post-content')
.first()
.should('contain', fakePost.content)
fakePost.tags.forEach((t, i) => {
cy
.visitApp()
.getByTestId('create-post-link')
.click()
.getByTestId('title-input')
.type(fakePost.title)
.getByTestId('content-input')
// the delay is because the content takes
// forever to type otherwise
.type(fakePost.content, {delay: 1})
.getByTestId('tags-input')
.type(fakePost.tags.join(', '))
.getByTestId('editor-submit')
.click()
assertRoute('/')
cy
.getByTestId('post-title')
.first()
.should('contain', fakePost.title)
cy
.getByTestId('post-content')
.first()
.should('contain', fakePost.content)
fakePost.tags.forEach((t, i) => {
cy
.getByTestId(`post-tag-${i}`)
.first()
.should('contain', t)
})
cy
.getByTestId('post-author-username')
.getByTestId(`post-tag-${i}`)
.first()
.should('contain', user.username)
.should('contain', t)
})
cy
.getByTestId('post-author-username')
.first()
.should('contain', user.username)
})
})
37 changes: 30 additions & 7 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
Cypress.Commands.add('visitApp', route => {
const clientUrl = Cypress.env('CLIENT_URL').trim()
const apiUrl = Cypress.env('API_URL').trim()
const query = `?api-url=${encodeURIComponent(apiUrl)}`
const fullUrl = [clientUrl, route, query].filter(Boolean).join('/')
return cy.visit(fullUrl)
})
import {generate} from '../utils'

Cypress.Commands.add('getByTestId', id => {
return cy.get(`[data-test="${id}"]`)
})

Cypress.Commands.add('logout', () => {
return cy
.window()
.its('localStorage')
.invoke('removeItem', 'token')
})

Cypress.Commands.add('createNewUser', () => {
const user = generate.loginForm()

return cy
.log('create a test new user', user)
.request('POST', `${Cypress.env('API_URL').trim()}/auth/register`, user)
.then(({body}) => {
return Object.assign({}, body.user, user)
})
})

Cypress.Commands.add('loginAsNewUser', () => {
return cy.createNewUser().then(user => {
window.localStorage.setItem('token', user.token)
return user
})
})

Cypress.Commands.add('assertRoute', route => {
cy.url().should('equal', `${window.location.origin}${route}`)
})
Loading

0 comments on commit 93fe350

Please sign in to comment.