build players page with React

This commit is contained in:
Pig Fang 2020-02-08 17:09:43 +08:00
parent d119b054be
commit 22f68cd9fc
15 changed files with 1133 additions and 311 deletions

View File

@ -55,6 +55,8 @@ class PlayerController extends Controller
'user.player.player-name-length',
['min' => option('player_name_length_min'), 'max' => option('player_name_length_max')]
),
'score' => auth()->user()->score,
'cost' => (int) option('score_per_player'),
]);
}

View File

@ -0,0 +1,17 @@
import React from 'react'
import { trans } from '@/scripts/i18n'
const ViewerSkeleton: React.FC = () => (
<div className="card">
<div className="card-header">
<div className="d-flex justify-content-between">
<h3 className="card-title">
<span>{trans('general.texturePreview')}</span>
</h3>
</div>
</div>
<div className="card-body"></div>
</div>
)
export default ViewerSkeleton

View File

@ -0,0 +1,33 @@
import { useState, useEffect } from 'react'
import * as fetch from '../net'
import { Texture, TextureType } from '../types'
type Response = fetch.ResponseBody<Texture>
export default function useTexture(): [
{ url: string; type: TextureType },
React.Dispatch<React.SetStateAction<number>>,
] {
const [tid, setTid] = useState(0)
const [url, setUrl] = useState('')
const [type, setType] = useState<TextureType>('steve')
useEffect(() => {
if (tid <= 0) {
setUrl('')
return
}
const getTexture = async () => {
const {
data: { hash, type },
} = await fetch.get<Response>(`/skinlib/info/${tid}`)
setUrl(`${blessing.base_url}/textures/${hash}`)
setType(type)
}
getTexture()
}, [tid])
return [{ url, type }, setTid]
}

View File

@ -5,10 +5,7 @@ const virtual = document.createElement('div')
export default [
{
path: '/',
module: [
() => import('../styles/home.styl'),
() => import('./home-page'),
],
module: [() => import('../styles/home.styl'), () => import('./home-page')],
},
{
path: 'user',
@ -20,7 +17,7 @@ export default [
<div className="card-body"></div>
<div className="card-footer">&nbsp;</div>
</div>
)
),
},
{
path: 'user/closet',
@ -29,8 +26,14 @@ export default [
},
{
path: 'user/player',
component: () => import('../views/user/Players.vue'),
el: virtual,
react: () => import('../views/user/Players'),
el: '#players-list',
frame: () => (
<div className="card">
<div className="card-header">&nbsp;</div>
<div className="card-body p-0"></div>
</div>
),
},
{
path: 'user/player/bind',
@ -39,9 +42,7 @@ export default [
},
{
path: 'user/profile',
module: [
() => import('../views/user/profile/index'),
],
module: [() => import('../views/user/profile/index')],
},
{
path: 'user/oauth/manage',
@ -50,9 +51,7 @@ export default [
},
{
path: 'admin',
module: [
() => import('../views/admin/Dashboard'),
],
module: [() => import('../views/admin/Dashboard')],
},
{
path: 'admin/users',
@ -71,9 +70,7 @@ export default [
},
{
path: 'admin/customize',
module: [
() => import('../views/admin/Customization'),
],
module: [() => import('../views/admin/Customization')],
},
{
path: 'admin/i18n',
@ -92,9 +89,7 @@ export default [
},
{
path: 'admin/update',
module: [
() => import('../views/admin/Update'),
],
module: [() => import('../views/admin/Update')],
},
{
path: 'auth/login',

View File

@ -0,0 +1,14 @@
export type Player = {
pid: number
name: string
tid_skin: number
tid_cape: number
}
export type Texture = {
tid: number
hash: string
type: TextureType
}
export type TextureType = 'steve' | 'alex' | 'cape'

View File

@ -1,292 +0,0 @@
<template>
<div>
<portal selector="#players-list" :disabled="disablePortal">
<div class="card card-primary">
<div class="card-body table-responsive p-0">
<table class="table table-hover">
<thead>
<tr>
<th>PID</th>
<th v-t="'general.player.player-name'" />
<th v-t="'user.player.operation'" />
</tr>
</thead>
<tbody>
<tr
v-for="(player, index) in players"
:key="player.pid"
class="player"
:class="{ 'player-selected': player.pid === selected }"
@click="preview(player)"
>
<td class="pid">{{ player.pid }}</td>
<td class="player-name">{{ player.name }}</td>
<td>
<button class="btn btn-default" @click="changeName(player)">
{{ $t('user.player.edit-pname') }}
</button>
<button
class="btn btn-warning"
data-toggle="modal"
data-target="#modal-clear-texture"
>
{{ $t('user.player.delete-texture') }}
</button>
<button class="btn btn-danger" @click="deletePlayer(player, index)">
{{ $t('user.player.delete-player') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card-footer">
<button class="btn btn-primary" data-toggle="modal" data-target="#modal-add-player">
<i class="fas fa-plus" aria-hidden="true" />&nbsp;{{ $t('user.player.add-player') }}
</button>
</div>
</div>
</portal>
<portal selector="#previewer" :disabled="disablePortal">
<previewer
v-if="using3dPreviewer"
:skin="skinUrl"
:cape="capeUrl"
:model="model"
title="user.player.player-info"
>
<template #footer>
<button class="btn btn-default" data-test="to2d" @click="togglePreviewer">
{{ $t('user.switch2dPreview') }}
</button>
</template>
</previewer>
<div v-else class="card">
<div class="card-header card-outline">
<!-- eslint-disable-next-line vue/no-v-html -->
<h3 class="card-title" v-html="$t('user.player.player-info')" />
</div>
<div class="card-body">
<div id="preview-2d">
<p>
{{ $t('general.skin') }}
<a v-if="preview2d.skin" :href="`${baseUrl}/skinlib/show/${preview2d.skin}`">
<img
class="skin2d"
:src="`${baseUrl}/preview/${preview2d.skin}?height=128`"
>
</a>
<span v-else v-t="'user.player.texture-empty'" class="skin2d" />
</p>
<p>
{{ $t('general.cape') }}
<a v-if="preview2d.cape" :href="`${baseUrl}/skinlib/show/${preview2d.cape}`">
<img
class="skin2d"
:src="`${baseUrl}/preview/${preview2d.cape}?height=128`"
>
</a>
<span v-else v-t="'user.player.texture-empty'" class="skin2d" />
</p>
</div>
</div>
<div class="card-footer">
<button class="btn btn-default" @click="togglePreviewer">
{{ $t('user.switch3dPreview') }}
</button>
</div>
</div>
</portal>
<portal selector="#modals" :disabled="disablePortal">
<add-player-dialog @add="fetchPlayers" />
<modal
id="modal-clear-texture"
:title="$t('user.chooseClearTexture')"
@confirm="clearTexture"
>
<label class="form-group">
<input v-model="clear.skin" type="checkbox"> {{ $t('general.skin') }}
</label>
<br>
<label class="form-group">
<input v-model="clear.cape" type="checkbox"> {{ $t('general.cape') }}
</label>
</modal>
</portal>
</div>
</template>
<script>
import Modal from '../../components/Modal.vue'
import Portal from '../../components/Portal'
import AddPlayerDialog from '../../components/AddPlayerDialog.vue'
import emitMounted from '../../components/mixins/emitMounted'
import { showModal, toast } from '../../scripts/notify'
import { truthy } from '../../scripts/validators'
export default {
name: 'Players',
components: {
AddPlayerDialog,
Modal,
Portal,
Previewer: () => import('../../components/Previewer.vue'),
},
mixins: [
emitMounted,
],
props: {
baseUrl: {
type: String,
default: blessing.base_url,
},
},
data() {
return {
players: [],
selected: 0,
using3dPreviewer: true,
skinUrl: '',
capeUrl: '',
model: 'steve',
preview2d: {
skin: 0,
cape: 0,
},
clear: {
skin: false,
cape: false,
},
disablePortal: process.env.NODE_ENV === 'test',
}
},
async beforeMount() {
await this.fetchPlayers()
if (this.players.length === 1) {
this.preview(this.players[0])
}
},
methods: {
async fetchPlayers() {
this.players = (await this.$http.get('/user/player/list')).data
},
togglePreviewer() {
this.using3dPreviewer = !this.using3dPreviewer
},
async preview(player) {
this.selected = player.pid
this.preview2d.skin = player.tid_skin
this.preview2d.cape = player.tid_cape
if (player.tid_skin) {
const { data: skin } = await this.$http.get(`/skinlib/info/${player.tid_skin}`)
this.skinUrl = `${this.baseUrl}/textures/${skin.hash}`
this.model = skin.type
} else {
this.skinUrl = ''
this.model = 'steve'
}
if (player.tid_cape) {
const { data: cape } = await this.$http.get(`/skinlib/info/${player.tid_cape}`)
this.capeUrl = `${this.baseUrl}/textures/${cape.hash}`
} else {
this.capeUrl = ''
}
},
async changeName(player) {
let value
try {
({ value } = await showModal({
mode: 'prompt',
text: this.$t('user.changePlayerName'),
input: player.name,
validator: truthy(this.$t('user.emptyPlayerName')),
}))
} catch {
return
}
const { code, message } = await this.$http.post(
`/user/player/rename/${player.pid}`,
{ name: value },
)
if (code === 0) {
toast.success(message)
player.name = value
} else {
toast.error(message)
}
},
async clearTexture() {
if (Object.values(this.clear).every(value => !value)) {
toast.error(this.$t('user.noClearChoice'))
return
}
const { code, message } = await this.$http.post(
`/user/player/texture/clear/${this.selected}`,
this.clear,
)
if (code === 0) {
toast.success(message)
const player = this.players.find(({ pid }) => pid === this.selected)
Object.keys(this.clear)
.filter(type => this.clear[type])
.forEach(type => (player[`tid_${type}`] = 0))
} else {
toast.error(message)
}
},
async deletePlayer(player, index) {
try {
await showModal({
title: this.$t('user.deletePlayer'),
text: this.$t('user.deletePlayerNotice'),
okButtonType: 'danger',
})
} catch {
return
}
const { code, message } = await this.$http.post(`/user/player/delete/${player.pid}`)
if (code === 0) {
this.$delete(this.players, index)
toast.success(message)
} else {
toast.error(message)
}
},
},
}
</script>
<style lang="stylus">
.player
cursor pointer
border-bottom 1px solid #f4f4f4
.pid, .player-name
padding-top 13px
.player:last-child
border-bottom none
.player-selected
background-color #f5f5f5
.skin2d
float right
max-height 64px
width 64px
font-size 16px
#preview-2d > p
height 64px
line-height 64px
</style>

View File

@ -0,0 +1,86 @@
import React, { useState } from 'react'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { toast } from '@/scripts/notify'
import { Player } from '@/scripts/types'
import Modal from '@/components/Modal'
type Extra = {
score: number
cost: number
rule: string
length: string
}
interface Props {
show: boolean
onAdd(player: Player): void
onClose(): void
}
const ModalAddPlayer: React.FC<Props> = props => {
const [name, setName] = useState('')
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value)
}
const handleConfirm = async () => {
const { code, message, data: player } = await fetch.post<
fetch.ResponseBody<Player>
>('/user/player/add', { name })
if (code === 0) {
toast.success(message)
props.onAdd(player)
} else {
toast.error(message)
}
}
const handleClose = () => {
setName('')
props.onClose()
}
const { score, cost, rule, length } = blessing.extra as Extra
const isScoreEnough = score >= cost
return (
<Modal
show={props.show}
title={t('user.player.add-player')}
onConfirm={handleConfirm}
onClose={handleClose}
>
<div className="form-group">
<label htmlFor="new-player-name">
{t('general.player.player-name')}
</label>
<input
type="text"
className="form-control"
id="new-player-name"
value={name}
onChange={handleNameChange}
/>
</div>
<div className="callout callout-info">
<ul className="m-0 p-0 pl-3">
<li>{rule}</li>
<li>{length}</li>
</ul>
</div>
<div
className={`alert alert-${isScoreEnough ? 'success' : 'danger'}`}
role="alert"
>
<i className={`icon fas fa-${isScoreEnough ? 'check' : 'times'}`}></i>
<span className="ml-1">
{t('user.cur-score')} {score}
</span>
</div>
</Modal>
)
}
export default ModalAddPlayer

View File

@ -0,0 +1,62 @@
import React, { useState } from 'react'
import { t } from '@/scripts/i18n'
import Modal from '@/components/Modal'
interface Props {
show: boolean
onSubmit(skin: boolean, cape: boolean): Promise<void>
onClose(): void
}
const ModalReset: React.FC<Props> = props => {
const [skin, setSkin] = useState(false)
const [cape, setCape] = useState(false)
const handleSkinChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSkin(event.target.checked)
}
const handleCapeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCape(event.target.checked)
}
const handleConfirm = () => {
props.onSubmit(skin, cape)
}
const handleClose = () => {
setSkin(false)
setCape(false)
props.onClose()
}
return (
<Modal
show={props.show}
title={t('user.chooseClearTexture')}
onConfirm={handleConfirm}
onClose={handleClose}
>
<label className="d-block">
<input
type="checkbox"
className="mr-2"
checked={skin}
onChange={handleSkinChange}
/>
{t('general.skin')}
</label>
<label className="d-block">
<input
type="checkbox"
className="mr-2"
checked={cape}
onChange={handleCapeChange}
/>
{t('general.cape')}
</label>
</Modal>
)
}
export default ModalReset

View File

@ -0,0 +1,55 @@
import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
import { t } from '@/scripts/i18n'
import ViewerSkeleton from '@/components/ViewerSkeleton'
import Viewer2d from './Viewer2d'
const Viewer3d = React.lazy(() => import('@/components/Viewer'))
interface Props {
skin: string
cape: string
isAlex: boolean
}
const container = document.createElement('div')
const Previewer: React.FC<Props> = props => {
const [is3d, setIs3d] = useState(true)
useEffect(() => {
const mount = document.querySelector('#previewer')!
mount.appendChild(container)
return () => {
mount.removeChild(container)
}
}, [])
const switchMode = () => setIs3d(is => !is)
const switcher = (
<button className="btn btn-default" onClick={switchMode}>
{is3d ? t('user.switch2dPreview') : t('user.switch3dPreview')}
</button>
)
const { skin, cape, isAlex } = props
return ReactDOM.createPortal(
is3d ? (
<React.Suspense fallback={<ViewerSkeleton />}>
<Viewer3d skin={skin} cape={cape} isAlex={isAlex}>
{switcher}
</Viewer3d>
</React.Suspense>
) : (
<Viewer2d skin={skin} cape={cape}>
{switcher}
</Viewer2d>
),
container
)
}
export default Previewer

View File

@ -0,0 +1,7 @@
.row {
cursor: pointer;
}
.selected {
background-color: #efefef;
}

View File

@ -0,0 +1,51 @@
import React from 'react'
import { t } from '@/scripts/i18n'
import ButtonEdit from '@/components/ButtonEdit'
import { Player } from '@/scripts/types'
import styles from './Row.scss'
interface Props {
player: Player
selected: boolean
onClick: React.MouseEventHandler
onEditName(player: Player): Promise<void>
onReset(): void
onDelete(player: Player): Promise<void>
}
const Row: React.FC<Props> = props => {
const { player } = props
const handleEdit = () => {
props.onEditName(player)
}
const handleDelete = () => {
props.onDelete(player)
}
const classes = [styles.row]
if (props.selected) {
classes.push(styles.selected)
}
return (
<tr className={classes.join(' ')} onClick={props.onClick}>
<td>{player.pid}</td>
<td>
<span>{player.name}</span>
<ButtonEdit title={t('user.player.edit-pname')} onClick={handleEdit} />
</td>
<td className="d-flex">
<button className="btn btn-warning" onClick={props.onReset}>
{t('user.player.delete-texture')}
</button>
<button className="btn btn-danger ml-2" onClick={handleDelete}>
{t('user.player.delete-player')}
</button>
</td>
</tr>
)
}
export default Row

View File

@ -0,0 +1,8 @@
.texture {
max-height: 64px;
width: 64px;
}
.line {
width: 80%;
}

View File

@ -0,0 +1,47 @@
import React from 'react'
import { t } from '@/scripts/i18n'
import styles from './Viewer2d.scss'
interface Props {
skin: string
cape: string
}
const Viewer2d: React.FC<Props> = props => {
return (
<div className="card">
<div className="card-header">
<h3 className="card-title">{t('general.texturePreview')}</h3>
</div>
<div className="card-body">
<div className={`d-flex justify-content-between mb-5 ${styles.line}`}>
<span>{t('general.skin')}</span>
{props.skin ? (
<img
src={props.skin}
className={styles.texture}
alt={t('general.skin')}
/>
) : (
<span>{t('user.player.texture-empty')}</span>
)}
</div>
<div className={`d-flex justify-content-between mt-5 ${styles.line}`}>
<span>{t('general.cape')}</span>
{props.cape ? (
<img
src={props.cape}
className={styles.texture}
alt={t('general.cape')}
/>
) : (
<span>{t('user.player.texture-empty')}</span>
)}
</div>
</div>
<div className="card-footer">{props.children}</div>
</div>
)
}
export default Viewer2d

View File

@ -0,0 +1,221 @@
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, toast } from '@/scripts/notify'
import useTexture from '@/scripts/hooks/useTexture'
import { Player } from '@/scripts/types'
import Loading from '@/components/Loading'
import Row from './Row'
import Previewer from './Previewer'
import ModalAddPlayer from './ModalAddPlayer'
import ModalReset from './ModalReset'
const Players: React.FC = () => {
const [players, setPlayers] = useState<Player[]>([])
const [isLoading, setIsLoading] = useState(false)
const [selected, setSelected] = useState(0)
const [skin, setSkin] = useTexture()
const [cape, setCape] = useTexture()
const [search, setSearch] = useState('')
const [showModalAddPlayer, setShowModalAddPlayer] = useState(false)
const [showModalReset, setShowModalReset] = useState(false)
const selectPlayer = (player: Player) => {
setSelected(player.pid)
setSkin(player.tid_skin)
setCape(player.tid_cape)
}
useEffect(() => {
const getPlayers = async () => {
setIsLoading(true)
const { data } = await fetch.get<fetch.ResponseBody<Player[]>>(
'/user/player/list',
)
setPlayers(data)
if (data.length === 1) {
selectPlayer(data[0])
}
setIsLoading(false)
}
getPlayers()
}, [])
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearch(event.target.value)
}
const handleAdd = (player: Player) => {
setPlayers(players => [...players, player])
}
const editName = async (player: Player, index: number) => {
let name: string
try {
const { value } = await showModal({
mode: 'prompt',
text: t('user.changePlayerName'),
input: player.name,
validator: (value: string) => {
if (!value) {
return t('user.emptyPlayerName')
}
},
})
name = value
} catch {
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
`/user/player/rename/${player.pid}`,
{ name },
)
if (code === 0) {
toast.success(message)
setPlayers(players => {
players[index] = { ...player, name }
return players.slice()
})
} else {
toast.error(message)
}
}
const resetTexture = async (skin: boolean, cape: boolean) => {
if (!skin && !cape) {
toast.warning(t('user.noClearChoice'))
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
`/user/player/texture/clear/${selected}`,
{ type: [skin && 'skin', cape && 'cape'].filter(Boolean) },
)
if (code === 0) {
toast.success(message)
if (skin) {
setSkin(0)
}
if (cape) {
setCape(0)
}
setPlayers(players => {
const index = players.findIndex(player => player.pid === selected)
const player = Object.assign({}, players[index])
if (skin) {
player.tid_skin = 0
}
if (cape) {
player.tid_cape = 0
}
players[index] = player
return players.slice()
})
} else {
toast.error(message)
}
}
const deletePlayer = async (player: Player) => {
try {
await showModal({
title: t('user.deletePlayer'),
text: t('user.deletePlayerNotice'),
okButtonType: 'danger',
})
} catch {
return
}
const { code, message } = await fetch.post(
`/user/player/delete/${player.pid}`,
)
if (code === 0) {
toast.success(message)
const { pid } = player
setPlayers(players => players.filter(player => player.pid !== pid))
} else {
toast.error(message)
}
}
const openModalAddPlayer = () => setShowModalAddPlayer(true)
const closeModalAddPlayer = () => setShowModalAddPlayer(false)
const openModalReset = () => setShowModalReset(true)
const closeModalReset = () => setShowModalReset(false)
return (
<>
<div className="card">
<div className="card-header">
<input
type="text"
className="form-control"
placeholder={t('user.typeToSearch')}
onChange={handleSearch}
/>
</div>
<div className="card-body p-0 table-responsive">
<table className="table table-hover">
<thead>
<tr>
<th style={{ width: '12%' }}>PID</th>
<th>{t('general.player.player-name')}</th>
<th style={{ width: '50%' }}>{t('user.player.operation')}</th>
</tr>
</thead>
<tbody>
{players.length === 0 ? (
<tr>
<td className="text-center" colSpan={3}>
{isLoading ? <Loading /> : 'Nothing here.'}
</td>
</tr>
) : (
players
.filter(({ name }) => name.includes(search))
.map((player, i) => (
<Row
key={player.pid}
player={player}
selected={selected === player.pid}
onClick={() => selectPlayer(player)}
onEditName={() => editName(player, i)}
onReset={openModalReset}
onDelete={deletePlayer}
/>
))
)}
</tbody>
</table>
</div>
<div className="card-footer">
<button className="btn btn-primary" onClick={openModalAddPlayer}>
<i className="fas fa-plus mr-1"></i>
<span>{t('user.player.add-player')}</span>
</button>
</div>
</div>
<Previewer
skin={skin.url}
cape={cape.url}
isAlex={skin.type === 'alex'}
/>
<ModalAddPlayer
show={showModalAddPlayer}
onAdd={handleAdd}
onClose={closeModalAddPlayer}
/>
<ModalReset
show={showModalReset}
onSubmit={resetTexture}
onClose={closeModalReset}
/>
</>
)
}
export default hot(Players)

View File

@ -0,0 +1,516 @@
import React from 'react'
import { render, fireEvent, wait } from '@testing-library/react'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { Player } from '@/scripts/types'
import Players from '@/views/user/Players'
jest.mock('@/scripts/net')
const fixture: Readonly<Player> = Object.freeze<Player>({
pid: 1,
name: 'kumiko',
tid_skin: 1,
tid_cape: 2,
})
beforeEach(() => {
const container = document.createElement('div')
container.id = 'previewer'
document.body.appendChild(container)
blessing.extra = {
rule: 'please follow the rule',
length: 'greater than 3',
score: 1000,
cost: 50,
}
})
afterEach(() => {
document.querySelector('#previewer')!.remove()
})
test('loading indicator', () => {
fetch.get.mockResolvedValue({ data: [] })
const { queryByTitle } = render(<Players />)
expect(queryByTitle('Loading...')).toBeInTheDocument()
})
test('search players', async () => {
const fixture2: Player = { pid: 2, name: 'reina', tid_skin: 3, tid_cape: 4 }
fetch.get.mockResolvedValue({ data: [fixture, fixture2] })
const { getByPlaceholderText, queryByText } = render(<Players />)
await wait()
fireEvent.input(getByPlaceholderText(t('user.typeToSearch')), {
target: { value: 'k' },
})
expect(queryByText(fixture.name)).toBeInTheDocument()
expect(queryByText(fixture2.name)).not.toBeInTheDocument()
})
describe('select player automatically', () => {
it('only one player', async () => {
fetch.get.mockResolvedValue({ data: [fixture] })
render(<Players />)
await wait()
expect(fetch.get).toBeCalledWith(`/skinlib/info/${fixture.tid_skin}`)
expect(fetch.get).toBeCalledWith(`/skinlib/info/${fixture.tid_cape}`)
})
it('more players', async () => {
const fixture2: Player = { pid: 2, name: 'reina', tid_skin: 3, tid_cape: 4 }
fetch.get.mockResolvedValue({ data: [fixture, fixture2] })
render(<Players />)
await wait()
expect(fetch.get).not.toBeCalledWith(`/skinlib/info/${fixture.tid_skin}`)
expect(fetch.get).not.toBeCalledWith(`/skinlib/info/${fixture.tid_cape}`)
})
})
describe('2d preview', () => {
it('skin and cape', async () => {
fetch.get
.mockResolvedValueOnce({ data: [fixture] })
.mockResolvedValueOnce({ data: { hash: 'a', type: 'steve' } })
.mockResolvedValueOnce({ data: { hash: 'b', type: 'cape' } })
const { getByAltText, getByText } = render(<Players />)
await wait()
fireEvent.click(getByText(t('user.switch2dPreview')))
expect(getByAltText(t('general.skin'))).toHaveAttribute(
'src',
`${blessing.base_url}/textures/a`,
)
expect(getByAltText(t('general.cape'))).toHaveAttribute(
'src',
`${blessing.base_url}/textures/b`,
)
})
it('skin only', async () => {
fetch.get
.mockResolvedValueOnce({ data: [{ ...fixture, tid_cape: 0 }] })
.mockResolvedValueOnce({ data: { hash: 'a', type: 'steve' } })
const { getByAltText, queryByAltText, getByText, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByText(t('user.switch2dPreview')))
expect(getByAltText(t('general.skin'))).toHaveAttribute(
'src',
`${blessing.base_url}/textures/a`,
)
expect(queryByAltText(t('general.cape'))).not.toBeInTheDocument()
expect(queryByText(t('user.player.texture-empty'))).toBeInTheDocument()
})
it('cape only', async () => {
fetch.get
.mockResolvedValueOnce({ data: [{ ...fixture, tid_skin: 0 }] })
.mockResolvedValueOnce({ data: { hash: 'a', type: 'cape' } })
const { getByAltText, queryByAltText, getByText, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByText(t('user.switch2dPreview')))
expect(getByAltText(t('general.cape'))).toHaveAttribute(
'src',
`${blessing.base_url}/textures/a`,
)
expect(queryByAltText(t('general.skin'))).not.toBeInTheDocument()
expect(queryByText(t('user.player.texture-empty'))).toBeInTheDocument()
})
})
describe('create player', () => {
beforeEach(() => {
fetch.get.mockResolvedValue({ data: [] })
})
it('alert if score is enough', async () => {
const { getByRole, getByText, queryByText } = render(<Players />)
await wait()
fireEvent.click(getByText(t('user.player.add-player')))
expect(
queryByText(`${t('user.cur-score')} ${blessing.extra.score}`),
).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-success')
})
it('alert if lack of score', async () => {
blessing.extra.score = 0
const { getByRole, getByText, queryByText } = render(<Players />)
await wait()
fireEvent.click(getByText(t('user.player.add-player')))
expect(
queryByText(`${t('user.cur-score')} ${blessing.extra.score}`),
).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
})
it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'success', data: fixture })
const { getByText, getByLabelText, getByRole, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByText(t('user.player.add-player')))
fireEvent.input(getByLabelText(t('general.player.player-name')), {
target: { value: fixture.name },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/user/player/add', {
name: fixture.name,
})
expect(queryByText('success')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
expect(queryByText(fixture.pid.toString())).toBeInTheDocument()
expect(queryByText(fixture.name)).toBeInTheDocument()
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByLabelText, getByRole, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByText(t('user.player.add-player')))
fireEvent.input(getByLabelText(t('general.player.player-name')), {
target: { value: fixture.name },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/user/player/add', {
name: fixture.name,
})
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
expect(queryByText(fixture.name)).not.toBeInTheDocument()
})
it('cancelled', async () => {
const { getByText, getByLabelText, queryByText } = render(<Players />)
await wait()
fireEvent.click(getByText(t('user.player.add-player')))
fireEvent.input(getByLabelText(t('general.player.player-name')), {
target: { value: fixture.name },
})
fireEvent.click(getByText(t('general.cancel')))
await wait()
expect(fetch.post).not.toBeCalled()
expect(queryByText(fixture.name)).not.toBeInTheDocument()
})
it('clear form on close', async () => {
const { getByText, getByLabelText } = render(<Players />)
await wait()
fireEvent.click(getByText(t('user.player.add-player')))
fireEvent.input(getByLabelText(t('general.player.player-name')), {
target: { value: fixture.name },
})
fireEvent.click(getByText(t('general.cancel')))
fireEvent.click(getByText(t('user.player.add-player')))
expect(getByLabelText(t('general.player.player-name'))).toHaveValue('')
})
})
describe('edit player name', () => {
beforeEach(() => {
fetch.get.mockResolvedValue({ data: [fixture] })
})
it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const {
getByText,
getByTitle,
getByDisplayValue,
getByRole,
queryByText,
} = render(<Players />)
await wait()
fireEvent.click(getByTitle(t('user.player.edit-pname')))
fireEvent.input(getByDisplayValue(fixture.name), {
target: { value: 'reina' },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith(`/user/player/rename/${fixture.pid}`, {
name: 'reina',
})
expect(queryByText('success')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
expect(queryByText('reina')).toBeInTheDocument()
})
it('empty name', async () => {
const { getByText, getByTitle, getByDisplayValue, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByTitle(t('user.player.edit-pname')))
fireEvent.input(getByDisplayValue(fixture.name), {
target: { value: '' },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).not.toBeCalled()
expect(queryByText(t('user.emptyPlayerName'))).toBeInTheDocument()
fireEvent.click(getByText(t('general.cancel')))
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const {
getByText,
getByTitle,
getByDisplayValue,
getByRole,
queryByText,
} = render(<Players />)
await wait()
fireEvent.click(getByTitle(t('user.player.edit-pname')))
fireEvent.input(getByDisplayValue(fixture.name), {
target: { value: 'reina' },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith(`/user/player/rename/${fixture.pid}`, {
name: 'reina',
})
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
expect(queryByText(fixture.name)).toBeInTheDocument()
})
it('cancelled', async () => {
const { getByText, getByTitle, getByDisplayValue, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByTitle(t('user.player.edit-pname')))
fireEvent.input(getByDisplayValue(fixture.name), {
target: { value: 'reina' },
})
fireEvent.click(getByText(t('general.cancel')))
await wait()
expect(fetch.post).not.toBeCalled()
expect(queryByText(fixture.name)).toBeInTheDocument()
})
})
describe('reset texture', () => {
beforeEach(() => {
fetch.get.mockResolvedValue({ data: [fixture] })
})
it('clear skin and cape', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const { getByText, getByRole, getByLabelText, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByText(t('user.player.delete-texture')))
fireEvent.click(getByLabelText(t('general.skin')))
fireEvent.click(getByLabelText(t('general.cape')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith(
`/user/player/texture/clear/${fixture.pid}`,
{
type: ['skin', 'cape'],
},
)
expect(queryByText('success')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
})
it('clear skin', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const { getByText, getByRole, getByLabelText, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByText(t('user.player.delete-texture')))
fireEvent.click(getByLabelText(t('general.skin')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith(
`/user/player/texture/clear/${fixture.pid}`,
{
type: ['skin'],
},
)
expect(queryByText('success')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
})
it('clear cape', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const { getByText, getByRole, getByLabelText, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByText(t('user.player.delete-texture')))
fireEvent.click(getByLabelText(t('general.cape')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith(
`/user/player/texture/clear/${fixture.pid}`,
{
type: ['cape'],
},
)
expect(queryByText('success')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
})
it('select nothing', async () => {
const { getByText, getByRole, queryByText } = render(<Players />)
await wait()
fireEvent.click(getByText(t('user.player.delete-texture')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).not.toBeCalled()
expect(queryByText(t('user.noClearChoice'))).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-warning')
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByRole, getByLabelText, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByText(t('user.player.delete-texture')))
fireEvent.click(getByLabelText(t('general.skin')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith(
`/user/player/texture/clear/${fixture.pid}`,
{
type: ['skin'],
},
)
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
})
it('cancelled', async () => {
const { getByText, getByRole, getByLabelText, queryByText } = render(
<Players />,
)
await wait()
fireEvent.click(getByText(t('user.player.delete-texture')))
fireEvent.click(getByLabelText(t('general.skin')))
fireEvent.click(getByText(t('general.cancel')))
await wait()
expect(fetch.post).not.toBeCalled()
})
})
describe('delete player', () => {
beforeEach(() => {
fetch.get.mockResolvedValue({ data: [fixture] })
})
it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const { getByText, getByRole, queryByText } = render(<Players />)
await wait()
fireEvent.click(getByText(t('user.player.delete-player')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith(`/user/player/delete/${fixture.pid}`)
expect(getByText('success')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
expect(queryByText(fixture.name)).not.toBeInTheDocument()
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByRole, queryByText } = render(<Players />)
await wait()
fireEvent.click(getByText(t('user.player.delete-player')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith(`/user/player/delete/${fixture.pid}`)
expect(getByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
expect(queryByText(fixture.name)).toBeInTheDocument()
})
it('cancelled', async () => {
const { getByText, getByRole, queryByText } = render(<Players />)
await wait()
fireEvent.click(getByText(t('user.player.delete-player')))
fireEvent.click(getByText(t('general.cancel')))
await wait()
expect(fetch.post).not.toBeCalled()
expect(queryByText(fixture.name)).toBeInTheDocument()
})
})