build "bind players" page with React

This commit is contained in:
Pig Fang 2020-02-09 11:05:02 +08:00
parent 6a7d9d0fde
commit ec948ef1c2
5 changed files with 175 additions and 127 deletions

View File

@ -37,7 +37,7 @@ export default [
},
{
path: 'user/player/bind',
component: () => import('../views/user/Bind.vue'),
react: () => import('../views/user/BindPlayers'),
el: 'form',
},
{

View File

@ -1,83 +0,0 @@
<template>
<form @submit.prevent="submit">
<div v-if="players.length">
<p v-t="'user.bindExistedPlayer'" />
<div class="form-group mb-3">
<select v-model="selected" class="form-control player-select">
<option v-for="name in players" :key="name">{{ name }}</option>
</select>
</div>
</div>
<div v-else>
<p v-t="'user.bindNewPlayer'" />
<div class="form-group mb-3">
<input
v-model="selected"
class="form-control"
:placeholder="$t('general.player.player-name')"
>
</div>
</div>
<div v-show="message" class="alert alert-warning" v-text="message" />
<button class="btn btn-primary float-right" type="submit" :disabled="pending">
<template v-if="pending">
<i class="fa fa-spinner fa-spin" /> {{ $t('general.wait') }}
</template>
<span v-else>{{ $t('general.submit') }}</span>
</button>
</form>
</template>
<script>
import emitMounted from '../../components/mixins/emitMounted'
import { showModal } from '../../scripts/notify'
export default {
name: 'BindPlayer',
mixins: [
emitMounted,
],
data() {
return {
players: [],
selected: '',
pending: false,
message: '',
}
},
mounted() {
this.fetchPlayers()
},
methods: {
async fetchPlayers() {
const players = (await this.$http.get('/user/player/list')).data
this.players = players.map(player => player.name)
this.selected = this.players[0]
},
async submit() {
this.pending = true
const { code, message } = await this.$http.post(
'/user/player/bind',
{ player: this.selected },
)
this.pending = false
if (code === 0) {
await showModal({ mode: 'alert', text: message })
window.location.href = `${blessing.base_url}/user`
} else {
this.message = message
}
},
},
}
</script>
<style lang="stylus">
@import "../../styles/auth.styl"
.player-select
width 100%
</style>

View File

@ -0,0 +1,98 @@
import React, { useState, useEffect } from 'react'
import { hot } from 'react-hot-loader/root'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { showModal } from '@/scripts/notify'
import { Player } from '@/scripts/types'
import Loading from '@/components/Loading'
const BindPlayers: React.FC = () => {
const [players, setPlayers] = useState<string[]>([])
const [selected, setSelected] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isPending, setIsPending] = useState(false)
useEffect(() => {
const getPlayers = async () => {
setIsLoading(true)
const response = await fetch.get<fetch.ResponseBody<Player[]>>(
'/user/player/list',
)
const players = response.data.map(player => player.name)
setPlayers(players)
setSelected(players[0])
setIsLoading(false)
}
getPlayers()
}, [])
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setIsPending(true)
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/user/player/bind',
{ player: selected },
)
if (code === 0) {
await showModal({ mode: 'alert', text: message })
window.location.href = `${blessing.base_url}/user`
} else {
showModal({ mode: 'alert', text: message })
}
setIsPending(false)
}
return isLoading ? (
<Loading />
) : (
<form method="post" onSubmit={handleSubmit}>
{players.length > 0 ? (
<>
<p>{t('user.bindExistedPlayer')}</p>
<div className="mb-3">
{players.map(player => (
<label className="d-block mb-1">
<input
key={player}
type="radio"
className="mr-2"
checked={selected === player}
onChange={() => setSelected(player)}
/>
{player}
</label>
))}
</div>
</>
) : (
<>
<p>{t('user.bindNewPlayer')}</p>
<input
type="text"
className="form-control mb-3"
placeholder={t('general.player.player-name')}
onChange={e => setSelected(e.target.value)}
/>
</>
)}
<button
className="btn btn-primary float-right"
type="submit"
disabled={isPending}
>
{isPending ? (
<>
<i className="fas fa-spinner fa-spin mr-1"></i>
{t('general.wait')}
</>
) : (
t('general.submit')
)}
</button>
</form>
)
}
export default hot(BindPlayers)

View File

@ -1,43 +0,0 @@
import Vue from 'vue'
import { mount } from '@vue/test-utils'
import { flushPromises } from '../../utils'
import { showModal } from '@/scripts/notify'
import Bind from '@/views/user/Bind.vue'
jest.mock('@/scripts/notify')
test('list existed players', async () => {
Vue.prototype.$http.get
.mockResolvedValue({ data: [{ name: 'a' }, { name: 'b' }] })
const wrapper = mount(Bind)
await flushPromises()
const options = wrapper.findAll('option')
expect(options).toHaveLength(2)
})
test('show input box', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [] })
const wrapper = mount(Bind)
await flushPromises()
const input = wrapper.find('input')
expect(input.exists()).toBeTrue()
})
test('submit', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [] })
Vue.prototype.$http.post
.mockResolvedValueOnce({ code: 1, message: 'fail' })
.mockResolvedValueOnce({ code: 0, message: 'ok' })
const wrapper = mount(Bind)
const form = wrapper.find('form')
wrapper.find('input').setValue('abc')
form.trigger('submit')
await flushPromises()
expect(wrapper.find('.alert').text()).toBe('fail')
form.trigger('submit')
await flushPromises()
expect(showModal).toBeCalledWith({ mode: 'alert', text: 'ok' })
})

View File

@ -0,0 +1,76 @@
import React from 'react'
import { render, fireEvent, wait } from '@testing-library/react'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import BindPlayers from '@/views/user/BindPlayers'
jest.mock('@/scripts/net')
test('loading indicator', () => {
fetch.get.mockResolvedValue({ data: [] })
const { queryByTitle } = render(<BindPlayers />)
expect(queryByTitle('Loading...')).toBeInTheDocument()
})
describe('submit', () => {
it('have existed players', async () => {
fetch.get.mockResolvedValue({
data: [{ name: 'kumiko' }, { name: 'reina' }],
})
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const { getByText, getByLabelText, queryByText } = render(<BindPlayers />)
await wait()
fireEvent.click(getByLabelText('reina'))
fireEvent.click(getByText(t('general.submit')))
await wait()
expect(fetch.post).toBeCalledWith('/user/player/bind', { player: 'reina' })
expect(queryByText('success')).toBeInTheDocument()
fireEvent.click(getByText(t('general.confirm')))
})
it('no existed players', async () => {
fetch.get.mockResolvedValue({ data: [] })
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const { getByText, getByPlaceholderText, queryByText } = render(
<BindPlayers />,
)
await wait()
fireEvent.input(getByPlaceholderText(t('general.player.player-name')), {
target: { value: 'kumiko' },
})
fireEvent.click(getByText(t('general.submit')))
await wait()
expect(fetch.post).toBeCalledWith('/user/player/bind', { player: 'kumiko' })
expect(queryByText('success')).toBeInTheDocument()
fireEvent.click(getByText(t('general.confirm')))
})
it('failed', async () => {
fetch.get.mockResolvedValue({ data: [] })
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByPlaceholderText, queryByText } = render(
<BindPlayers />,
)
await wait()
fireEvent.input(getByPlaceholderText(t('general.player.player-name')), {
target: { value: 'kumiko' },
})
fireEvent.click(getByText(t('general.submit')))
await wait()
expect(fetch.post).toBeCalledWith('/user/player/bind', { player: 'kumiko' })
expect(queryByText('failed')).toBeInTheDocument()
fireEvent.click(getByText(t('general.confirm')))
})
})