rewrite texture detail page with React

This commit is contained in:
Pig Fang 2020-03-20 16:19:18 +08:00
parent 2c313ebde2
commit b6c58ef23f
26 changed files with 1209 additions and 1583 deletions

View File

@ -150,10 +150,7 @@ class SkinlibController extends Controller
'widgets' => [
[
['shared.previewer'],
[
'skinlib.widgets.show.info',
'skinlib.widgets.show.operations',
],
['skinlib.widgets.show.side'],
],
],
];

View File

@ -1,103 +0,0 @@
<template>
<modal
id="modal-use-as"
ref="modal"
:title="$t('user.closet.use-as.title')"
:ok-button-text="$t('general.submit')"
flex-footer
>
<template v-if="players.length !== 0">
<div class="form-group">
<input
v-model="search"
type="text"
class="form-control"
:placeholder="$t('user.typeToSearch')"
>
</div>
<button
v-for="player in filteredPlayers"
:key="player.pid"
class="btn btn-block btn-outline-info text-left"
@click="submit(player.pid)"
>
<img :src="avatarUrl(player)" width="45" height="45">&nbsp;
<span>{{ player.name }}</span>
</button>
</template>
<p v-else v-t="'user.closet.use-as.empty'" />
<template #footer>
<a
v-if="allowAdd"
v-t="'user.closet.use-as.add'"
data-toggle="modal"
data-target="#modal-add-player"
class="btn btn-default"
href="#"
/>
<button class="btn btn-default" data-dismiss="modal">
{{ $t('general.cancel') }}
</button>
</template>
</modal>
</template>
<script>
import $ from 'jquery'
import Modal from './Modal.vue'
import { toast } from '../scripts/notify'
export default {
name: 'ApplyToPlayerDialog',
components: {
Modal,
},
props: {
skin: Number,
cape: Number,
allowAdd: {
type: Boolean,
default: true,
},
},
data() {
return {
players: [],
search: '',
}
},
computed: {
filteredPlayers() {
return this.players.filter(player => player.name.includes(this.search))
},
},
methods: {
async fetchList() {
this.players = (await this.$http.get('/user/player/list')).data
},
async submit(selected) {
if (!this.skin && !this.cape) {
toast.info(this.$t('user.emptySelectedTexture'))
return
}
const { code, message } = await this.$http.post(
`/user/player/set/${selected}`,
{
skin: this.skin || undefined,
cape: this.cape || undefined,
},
)
if (code === 0) {
toast.success(message)
$('#modal-use-as').modal('hide')
} else {
toast.error(message)
}
},
avatarUrl(player) {
return `${blessing.base_url}/avatar/${player.tid_skin}?3d&size=45`
},
},
}
</script>

View File

@ -21,6 +21,7 @@ const ModalInput: React.FC<Props> = props => (
name="modal-radios"
id={`modal-radio-${choice.value}`}
value={choice.value}
checked={choice.value === props.value}
onChange={props.onChange}
/>
<label htmlFor={`modal-radio-${choice.value}`} className="ml-1">

View File

@ -1,36 +0,0 @@
import Vue from 'vue'
export default Vue.extend({
name: 'Portal',
props: {
selector: {
required: true,
type: String,
},
tag: {
type: String,
default: 'div',
},
disabled: {
type: Boolean,
default: false,
},
},
mounted() {
if (this.disabled) {
return
}
const container = document.querySelector(this.selector)
if (container) {
if (container.firstChild) {
container.replaceChild(this.$el, container.firstChild)
} else {
container.appendChild(this.$el)
}
}
},
render(h) {
return h(this.tag, [this.$slots.default])
},
})

View File

@ -1,178 +0,0 @@
<template>
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between">
<h3 class="card-title">
<span v-html="$t(title)" /> <!-- eslint-disable-line vue/no-v-html -->
<span class="badge bg-olive">{{ indicator }}</span>
</h3>
<div class="operations">
<i
data-toggle="tooltip"
data-placement="bottom"
:title="$t('general.walk') + ' / ' + $t('general.run')"
class="fas fa-forward"
@click="toggleRun"
/>
<i
data-toggle="tooltip"
data-placement="bottom"
:title="$t('general.rotation')"
class="fas fa-redo-alt"
@click="toggleRotate"
/>
<i
data-toggle="tooltip"
data-placement="bottom"
:title="$t('general.pause')"
class="fas"
:class="{ 'fa-pause': !paused, 'fa-play': paused }"
@click="togglePause"
/>
<i
data-toggle="tooltip"
data-placement="bottom"
:title="$t('general.reset')"
class="fas fa-stop"
@click="reset"
/>
</div>
</div>
</div>
<div class="card-body">
<div ref="previewer" class="previewer-3d">
<!-- Container for 3D Preview -->
</div>
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
<script>
import * as skinview3d from 'skinview3d'
import { emit } from '../scripts/event'
import SkinSteve from '../../../misc/textures/steve.png'
export default {
name: 'Previewer',
props: {
skin: String,
cape: String,
model: {
type: String,
default: 'steve',
},
closetMode: Boolean,
title: {
type: String,
default: 'general.texturePreview',
},
initPositionZ: {
type: Number,
default: 70,
},
},
data: () => ({
paused: false,
}),
computed: {
indicator() {
if (!this.closetMode) {
return ''
}
if (this.skin && this.cape) {
return `${this.$t('general.skin')} & ${this.$t('general.cape')}`
} else if (this.skin) {
return this.$t('general.skin')
} else if (this.cape) {
return this.$t('general.cape')
}
return ''
},
},
watch: {
skin(url) {
this.viewer.skinUrl = url || SkinSteve
},
cape(url) {
if (!url) {
this.viewer.playerObject.cape.visible = false
return
}
this.viewer.capeUrl = url
},
model(value) {
this.viewer.playerObject.skin.slim = value === 'alex'
},
},
mounted() {
this.initPreviewer()
emit('skinViewerMounted', this.$refs.previewer)
},
beforeDestroy() {
this.viewer.dispose()
},
methods: {
initPreviewer() {
this.viewer = new skinview3d.SkinViewer({
domElement: this.$refs.previewer,
width: this.$refs.previewer.clientWidth,
height: this.$refs.previewer.clientHeight,
detectModel: false,
skinUrl: this.skin || SkinSteve,
capeUrl: this.cape,
})
this.viewer.camera.position.z = this.initPositionZ
this.viewer.animation = new skinview3d.CompositeAnimation()
this.handles = {
walk: this.viewer.animation.add(skinview3d.WalkingAnimation),
run: this.viewer.animation.add(skinview3d.RunningAnimation),
rotate: this.viewer.animation.add(skinview3d.RotatingAnimation),
}
this.handles.run.paused = true
this.control = skinview3d.createOrbitControls(this.viewer)
},
togglePause() {
this.paused = !this.paused
this.viewer.animationPaused = !this.viewer.animationPaused
},
toggleRun() {
this.handles.run.paused = !this.handles.run.paused
this.handles.walk.paused = false
},
toggleRotate() {
this.handles.rotate.paused = !this.handles.rotate.paused
},
reset() {
this.viewer.dispose()
this.initPreviewer()
this.handles.walk.paused = true
this.handles.run.paused = true
this.handles.rotate.paused = true
this.viewer.camera.position.z = 70
this.viewer.playerObject.skin.slim = this.model === 'alex'
},
},
}
</script>
<style lang="stylus">
@media (min-width: 992px)
.previewer-3d
min-height 500px
.previewer-3d canvas
cursor move
.operations
i
padding .5em .5em
display inline
&:hover
color #555
cursor pointer
</style>

View File

@ -1,35 +0,0 @@
import Vue from 'vue'
import { showModal, toast } from '../../scripts/notify'
export default Vue.extend<{
tid: number
}, { setAsAvatar(): Promise<void> }, {}>({
methods: {
async setAsAvatar() {
try {
await showModal({
title: this.$t('user.setAvatar'),
text: this.$t('user.setAvatarNotice'),
})
} catch {
return
}
const { code, message } = await this.$http.post(
'/user/profile/avatar',
{ tid: this.tid },
)
if (code === 0) {
toast.success(message!)
Array
.from(
document.querySelectorAll<HTMLImageElement>('[alt="User Image"]'),
)
.forEach(el => (el.src += `?${new Date().getTime()}`))
} else {
toast.error(message!)
}
},
},
})

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
export default function useBlessingExtra<T>(key: string): T {
const [value, setValue] = useState<T>({} as T)
export default function useBlessingExtra<T>(key: string, defaultValue?: T): T {
const [value, setValue] = useState<T>(defaultValue!)
useEffect(() => {
setValue(blessing.extra[key] as T)

View File

@ -0,0 +1,16 @@
import { useEffect, useMemo } from 'react'
export default function useMount(selector: string): HTMLElement {
const container = useMemo(() => document.createElement('div'), [])
useEffect(() => {
const mount = document.querySelector(selector)!
mount.appendChild(container)
return () => {
mount.removeChild(container)
}
}, [])
return container
}

View File

@ -1,7 +1,5 @@
import React from 'react'
const virtual = document.createElement('div')
export default [
{
path: '/',
@ -118,8 +116,8 @@ export default [
},
{
path: 'skinlib/show/(\\d+)',
component: () => import('../views/skinlib/Show.vue'),
el: virtual,
react: () => import('../views/skinlib/Show'),
el: '#side',
},
{
path: 'skinlib/upload',

View File

@ -1,442 +0,0 @@
<template>
<div>
<portal selector="#previewer" :disabled="disablePortal">
<previewer
:skin="type !== 'cape' && textureUrl"
:cape="type === 'cape' ? textureUrl : ''"
:model="type"
:init-position-z="60"
>
<template #footer>
<button
v-if="anonymous"
class="btn btn-outline-secondary"
disabled
:title="$t('skinlib.show.anonymous')"
>
{{ $t('skinlib.addToCloset') }}
</button>
<div v-else class="d-flex justify-content-between">
<div>
<button
v-if="liked"
class="btn btn-outline-success mr-2"
data-toggle="modal"
data-target="#modal-use-as"
@click="fetchPlayersList"
>
{{ $t('skinlib.apply') }}
</button>
<button
v-if="liked"
class="btn btn-outline-primary mr-2"
data-test="removeFromCloset"
@click="removeFromCloset"
>
{{ $t('skinlib.removeFromCloset') }}
</button>
<button
v-else
class="btn btn-outline-primary mr-2"
data-test="addToCloset"
@click="addToCloset"
>
{{ $t('skinlib.addToCloset') }}
</button>
<button
v-if="type !== 'cape'"
class="btn btn-outline-info mr-2"
data-test="setAsAvatar"
@click="setAsAvatar"
>
{{ $t('user.setAsAvatar') }}
</button>
<button
v-if="canBeDownloaded"
class="btn btn-outline-info mr-2"
data-test="download"
@click="download"
>
{{ $t('skinlib.show.download') }}
</button>
<button
class="btn btn-outline-info mr-2"
data-test="report"
@click="report"
>
{{ $t('skinlib.report.title') }}
</button>
</div>
<div
class="pt-2 likes"
:class="[liked ? 'text-red' : 'text-gray']"
:title="$t('skinlib.show.likes')"
>
<i class="fas fa-heart" />
<span>{{ likes }}</span>
</div>
</div>
</template>
</previewer>
</portal>
<portal selector="#texture-info" :disabled="disablePortal">
<div class="card card-primary">
<div class="card-header">
<h3 v-t="'skinlib.show.detail'" class="card-title" />
</div>
<div class="card-body">
<table class="table">
<tbody>
<tr>
<td v-t="'skinlib.show.name'" />
<td>
{{ name|truncate }}
<small v-if="hasEditPermission">
<a v-t="'skinlib.show.edit'" href="#" @click="changeTextureName" />
</small>
</td>
</tr>
<tr>
<td v-t="'skinlib.show.model'" />
<td>
<span v-if="type === 'cape'">{{ $t('general.cape') }}</span>
<span v-else>{{ type }}</span>
<small v-if="hasEditPermission">
<a
href="#"
data-toggle="modal"
data-target="#modal-type"
@click="editingType = type"
>
{{ $t('skinlib.show.edit') }}
</a>
</small>
</td>
</tr>
<tr>
<td>Hash</td>
<td>
<span :title="hash">{{ hash.slice(0, 15) }}...</span>
</td>
</tr>
<tr>
<td v-t="'skinlib.show.size'" />
<td>{{ size }} KB</td>
</tr>
<tr>
<td v-t="'skinlib.show.uploader'" />
<td v-if="uploaderNickName !== null">
<a
:href="`${baseUrl}/skinlib?filter=${type === 'cape' ? 'cape' : 'skin'}&uploader=${uploader}`"
>{{ uploaderNickName }}</a>
<br>
<span
v-for="(badge, i) in badges"
:key="i"
class="badge mr-2"
:class="`bg-${badge.color}`"
>{{ badge.text }}</span>
</td>
<td v-else><span v-t="'general.unexistent-user'" /></td>
</tr>
<tr>
<td v-t="'skinlib.show.upload-at'" />
<td>{{ uploadAt }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</portal>
<portal selector="#operations" :disabled="disablePortal">
<div v-if="hasEditPermission" class="card card-warning">
<div class="card-header">
<h3 v-t="'admin.operationsTitle'" class="card-title" />
</div>
<div class="card-body">
<p v-t="'skinlib.show.manage-notice'" />
</div>
<div class="card-footer">
<div class="container d-flex justify-content-between">
<button class="btn btn-warning" @click="togglePrivacy">
{{ $t(togglePrivacyText) }}
</button>
<button class="btn btn-danger" @click="deleteTexture">
{{ $t('skinlib.show.delete-texture') }}
</button>
</div>
</div>
</div>
</portal>
<portal selector="#modals" :disabled="disablePortal">
<apply-to-player-dialog
ref="useAs"
:allow-add="false"
:skin="type !== 'cape' ? tid : 0"
:cape="type === 'cape' ? tid : 0"
/>
<modal
id="modal-type"
:title="$t(this.$t('skinlib.setNewTextureModel'))"
center
@confirm="changeModel"
>
<label class="mr-3">
<input
v-model="editingType"
type="radio"
name="type"
value="steve"
>
Steve
</label>
<label class="mr-3">
<input
v-model="editingType"
type="radio"
name="type"
value="alex"
>
Alex
</label>
<label>
<input
v-model="editingType"
type="radio"
name="type"
value="cape"
>
{{ $t('general.cape') }}
</label>
</modal>
</portal>
</div>
</template>
<script>
import Modal from '../../components/Modal.vue'
import Portal from '../../components/Portal'
import setAsAvatar from '../../components/mixins/setAsAvatar'
import addClosetItem from '../../components/mixins/addClosetItem'
import removeClosetItem from '../../components/mixins/removeClosetItem'
import emitMounted from '../../components/mixins/emitMounted'
import truncateText from '../../components/mixins/truncateText'
import ApplyToPlayerDialog from '../../components/ApplyToPlayerDialog.vue'
import { showModal, toast } from '../../scripts/notify'
import { truthy } from '../../scripts/validators'
export default {
name: 'Show',
components: {
ApplyToPlayerDialog,
Modal,
Portal,
Previewer: () => import('../../components/Previewer.vue'),
},
mixins: [
emitMounted,
addClosetItem,
removeClosetItem,
setAsAvatar,
truncateText,
],
props: {
baseUrl: {
type: String,
default: blessing.base_url,
},
},
data() {
return {
tid: +this.$route[1],
name: '',
type: 'steve',
likes: 0,
hash: '',
uploader: 0,
size: 0,
uploadAt: '',
public: true,
editingType: 'steve',
liked: blessing.extra.inCloset,
canBeDownloaded: blessing.extra.download,
currentUid: blessing.extra.currentUid,
admin: blessing.extra.admin,
uploaderNickName: blessing.extra.nickname,
reportScore: blessing.extra.report,
badges: blessing.extra.badges,
disablePortal: process.env.NODE_ENV === 'test',
}
},
computed: {
anonymous() {
return !this.currentUid
},
hasEditPermission() {
return this.uploader === this.currentUid || this.admin
},
togglePrivacyText() {
return this.public ? 'skinlib.setAsPrivate' : 'skinlib.setAsPublic'
},
textureUrl() {
return `${this.baseUrl}/textures/${this.hash}`
},
},
beforeMount() {
this.fetchData()
},
methods: {
async fetchData() {
const { data = {} } = await this.$http.get(`/skinlib/info/${this.tid}`)
Object.assign(this.$data, data)
this.uploadAt = data.upload_at
},
async addToCloset() {
this.$once('like-toggled', () => {
this.liked = true
this.likes += 1
})
await this.addClosetItem()
},
async removeFromCloset() {
this.$once('item-removed', () => {
this.liked = false
this.likes -= 1
})
await this.removeClosetItem()
},
download() {
const a = document.createElement('a')
a.href = `${this.baseUrl}/raw/${this.tid}`
a.download = `${this.name}.png`
const event = new MouseEvent('click')
a.dispatchEvent(event)
},
async changeTextureName() {
let value
try {
({ value } = await showModal({
mode: 'prompt',
text: this.$t('skinlib.setNewTextureName'),
input: this.name,
validator: truthy(this.$t('skinlib.emptyNewTextureName')),
}))
} catch (_) {
return
}
const { code, message } = await this.$http.post(
'/skinlib/rename',
{ tid: this.tid, new_name: value },
)
if (code === 0) {
this.name = value
toast.success(message)
} else {
toast.error(message)
}
},
async changeModel() {
const { code, message } = await this.$http.post(
'/skinlib/model',
{ tid: this.tid, model: this.editingType },
)
if (code === 0) {
this.type = this.editingType
toast.success(message)
} else {
toast.error(message)
}
},
async togglePrivacy() {
try {
await showModal({
text: this.public
? this.$t('skinlib.setPrivateNotice')
: this.$t('skinlib.setPublicNotice'),
})
} catch (_) {
return
}
const { code, message } = await this.$http.post(
'/skinlib/privacy',
{ tid: this.tid },
)
if (code === 0) {
toast.success(message)
this.public = !this.public
} else {
toast.error(message)
}
},
async deleteTexture() {
try {
await showModal({
text: this.$t('skinlib.deleteNotice'),
okButtonType: 'danger',
})
} catch (_) {
return
}
const { code, message } = await this.$http.post(
'/skinlib/delete',
{ tid: this.tid },
)
if (code === 0) {
toast.success(message)
setTimeout(() => (window.location = `${this.baseUrl}/skinlib`), 1000)
} else {
toast.error(message)
}
},
async report() {
const prompt = (() => {
if (this.reportScore > 0) {
return this.$t('skinlib.report.positive', { score: this.reportScore })
} else if (this.reportScore < 0) {
return this.$t('skinlib.report.negative', { score: -this.reportScore })
}
return ''
})()
let reason
try {
({ value: reason } = await showModal({
mode: 'prompt',
title: this.$t('skinlib.report.title'),
text: prompt,
placeholder: this.$t('skinlib.report.reason'),
}))
} catch (_) {
return
}
const { code, message } = await this.$http.post(
'/skinlib/report',
{ tid: this.tid, reason },
)
if (code === 0) {
toast.success(message)
} else {
toast.error(message)
}
},
fetchPlayersList() {
this.$refs.useAs.fetchList()
},
},
}
</script>
<style lang="stylus">
.table > tbody > tr > td
border-top 0
&:first-child
min-width 30%
</style>

View File

@ -0,0 +1,38 @@
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { showModal, toast } from '@/scripts/notify'
import { Texture } from '@/scripts/types'
export default async function addClosetItem(
texture: Texture,
): Promise<boolean> {
let name: string
try {
const { value } = await showModal({
mode: 'prompt',
title: t('skinlib.setItemName'),
text: t('skinlib.applyNotice'),
input: texture.name,
validator: (value: string) => {
if (!value) {
return t('skinlib.emptyItemName')
}
},
})
name = value
} catch {
return false
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/user/closet/add',
{ tid: texture.tid, name },
)
if (code === 0) {
toast.success(message)
} else {
toast.error(message)
}
return code === 0
}

View File

@ -0,0 +1,434 @@
import React, { useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { hot } from 'react-hot-loader/root'
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra'
import useMount from '@/scripts/hooks/useMount'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { showModal, toast } from '@/scripts/notify'
import { Texture, TextureType } from '@/scripts/types'
import ButtonEdit from '@/components/ButtonEdit'
import ViewerSkeleton from '@/components/ViewerSkeleton'
import ModalApply from '@/views/user/Closet/ModalApply'
import removeClosetItem from '@/views/user/Closet/removeClosetItem'
import setAsAvatar from '@/views/user/Closet/setAsAvatar'
import addClosetItem from './addClosetItem'
import styles from './styles.module.scss'
export type Badge = {
color: string
text: string
}
const Previewer = React.lazy(() => import('@/components/Viewer'))
const Show: React.FC = () => {
const [texture, setTexture] = useState<Texture>({} as Texture)
const [showModalApply, setShowModalApply] = useState(false)
const [liked, setLiked] = useState(false)
const nickname = useBlessingExtra<string | null>('nickname')
const currentUid = useBlessingExtra<number>('currentUid')
const isAdmin = useBlessingExtra<boolean>('admin')
const badges = useBlessingExtra<Badge[]>('badges', [])
const canBeDownloaded = useBlessingExtra<boolean>('download')
const reportScore = useBlessingExtra<number>('report')
const container = useMount('#previewer')
useEffect(() => {
const fetchInfo = async () => {
const url = location.href
.replace(blessing.base_url, '')
.replace('show', 'info')
const { data } = await fetch.get<fetch.ResponseBody<Texture>>(url)
setTexture(data)
}
fetchInfo()
}, [])
useEffect(() => {
setLiked(blessing.extra.inCloset as boolean)
}, [])
const handleEditName = async () => {
let name: string
try {
const { value } = await showModal({
mode: 'prompt',
text: t('skinlib.setNewTextureName'),
input: texture.name,
validator: (value: string) => {
if (!value) {
return t('skinlib.emptyNewTextureName')
}
},
})
name = value
} catch {
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/skinlib/rename',
{
tid: texture.tid,
new_name: name,
},
)
if (code === 0) {
toast.success(message)
setTexture(texture => ({ ...texture, name }))
} else {
toast.error(message)
}
}
const handleSwitchType = async () => {
let type: TextureType
try {
const { value } = await showModal({
mode: 'prompt',
text: t('skinlib.setNewTextureModel'),
input: texture.type,
inputType: 'radios',
choices: [
{ text: 'Steve', value: 'steve' },
{ text: 'Alex', value: 'alex' },
{ text: t('general.cape'), value: 'cape' },
],
})
type = value as TextureType
} catch {
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/skinlib/model',
{
tid: texture.tid,
model: type,
},
)
if (code === 0) {
toast.success(message)
setTexture(texture => ({ ...texture, type }))
} else {
toast.error(message)
}
}
const handleAddItemClick = async () => {
const ok = await addClosetItem(texture)
if (ok) {
setTexture(texture => ({ ...texture, likes: texture.likes + 1 }))
setLiked(true)
}
}
const handleRemoveItemClick = async () => {
const ok = await removeClosetItem(texture.tid)
if (ok) {
setTexture(texture => ({ ...texture, likes: texture.likes - 1 }))
setLiked(false)
}
}
const handleSetAsAvatar = () => setAsAvatar(texture.tid)
const handleDownloadClick = () => {
const a = document.createElement('a')
a.href = `${blessing.base_url}/raw/${texture.tid}`
a.download = `${texture.name}.png`
a.click()
}
const handleReport = async () => {
const prompt = (() => {
if (reportScore > 0) {
return t('skinlib.report.positive', { score: reportScore })
} else if (reportScore < 0) {
return t('skinlib.report.negative', { score: -reportScore })
}
return ''
})()
let reason: string
try {
const { value } = await showModal({
mode: 'prompt',
title: t('skinlib.report.title'),
text: prompt,
placeholder: t('skinlib.report.reason'),
})
reason = value
} catch {
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/skinlib/report',
{
tid: texture.tid,
reason,
},
)
if (code === 0) {
toast.success(message)
} else {
toast.error(message)
}
}
const handlePrivacyClick = async () => {
try {
await showModal({
text: texture.public
? t('skinlib.setPrivateNotice')
: t('skinlib.setPublicNotice'),
})
} catch {
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/skinlib/privacy',
{ tid: texture.tid },
)
if (code === 0) {
toast.success(message)
setTexture(texture => ({ ...texture, public: !texture.public }))
} else {
toast.error(message)
}
}
const handleDeleteTextureClick = async () => {
try {
await showModal({
text: t('skinlib.deleteNotice'),
okButtonType: 'danger',
})
} catch {
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/skinlib/delete',
{ tid: texture.tid },
)
if (code === 0) {
toast.success(message)
setTimeout(() => {
window.location.href = `${blessing.base_url}/skinlib`
}, 2000)
} else {
toast.error(message)
}
}
const handleOpenModalApply = () => setShowModalApply(true)
const handleCloseModalApply = () => setShowModalApply(false)
const linkToUploader = (() => {
const search = new URLSearchParams()
search.append('filter', texture.type === 'cape' ? 'cape' : 'skin')
search.append('uploader', texture.uploader?.toString())
return `${blessing.base_url}/skinlib?${search}`
})()
const canEdit = currentUid === texture.uploader || isAdmin
const textureUrl = `${blessing.base_url}/textures/${texture.hash}`
return (
<>
{createPortal(
<React.Suspense fallback={<ViewerSkeleton />}>
<Previewer
{...{ [texture.type === 'cape' ? 'cape' : 'skin']: textureUrl }}
isAlex={texture.type === 'alex'}
initPositionZ={60}
>
{currentUid === 0 ? (
<button
className="btn btn-outline-secondary"
title={t('skinlib.show.anonymous')}
disabled
>
{t('skinlib.addToCloset')}
</button>
) : (
<div className="d-flex justify-content-between align-items-center">
<div>
{liked && (
<button
className="btn btn-outline-success mr-2"
onClick={handleOpenModalApply}
>
{t('skinlib.apply')}
</button>
)}
{liked ? (
<button
className="btn btn-outline-primary mr-2"
onClick={handleRemoveItemClick}
>
{t('skinlib.removeFromCloset')}
</button>
) : (
<button
className="btn btn-outline-primary mr-2"
onClick={handleAddItemClick}
>
{t('skinlib.addToCloset')}
</button>
)}
{texture.type !== 'cape' && (
<button
className="btn btn-outline-info mr-2"
onClick={handleSetAsAvatar}
>
{t('user.setAsAvatar')}
</button>
)}
{canBeDownloaded && (
<button
className="btn btn-outline-info mr-2"
onClick={handleDownloadClick}
>
{t('skinlib.show.download')}
</button>
)}
<button
className="btn btn-outline-info mr-2"
onClick={handleReport}
>
{t('skinlib.report.title')}
</button>
</div>
<div
className={liked ? 'text-red' : 'text-gray'}
title={t('skinlib.show.likes')}
>
<i className="fas fa-heart mr-1" />
<span>{texture.likes}</span>
</div>
</div>
)}
</Previewer>
</React.Suspense>,
container,
)}
<div className="card card-primary">
<div className="card-header">
<h3 className="card-title">{t('skinlib.show.detail')}</h3>
</div>
<div className="card-body">
<div className="container">
<div className="row mt-2 mb-4">
<div className="col-4">{t('skinlib.show.name')}</div>
<div className={`col-7 ${styles.truncate}`} title={texture.name}>
{texture.name}
</div>
{canEdit && (
<div className="col-1">
<ButtonEdit
title={t('skinlib.show.edit')}
onClick={handleEditName}
/>
</div>
)}
</div>
<div className="row my-4">
<div className="col-4">{t('skinlib.show.model')}</div>
<div className="col-7">
{texture.type === 'cape' ? t('general.cape') : texture.type}
</div>
{canEdit && (
<div className="col-1">
<ButtonEdit
title={t('skinlib.show.edit')}
onClick={handleSwitchType}
/>
</div>
)}
</div>
<div className="row my-4">
<div className="col-4">Hash</div>
<div className={`col-8 ${styles.truncate}`} title={texture.hash}>
{texture.hash}
</div>
</div>
<div className="row my-4">
<div className="col-4">{t('skinlib.show.size')}</div>
<div className="col-8">{texture.size} KB</div>
</div>
<div className="row my-4">
<div className="col-4">{t('skinlib.show.uploader')}</div>
<div className={`col-8 ${styles.truncate}`}>
{nickname !== null ? (
<>
<div>
<a href={linkToUploader} target="_blank">
{nickname}
</a>
</div>
<div>
{badges.map(badge => (
<span
className={`badge bg-${badge.color} mr-2`}
key={badge.text}
>
{badge.text}
</span>
))}
</div>
</>
) : (
t('general.unexistent-user')
)}
</div>
</div>
<div className="row mt-4 mb-2">
<div className="col-4">{t('skinlib.show.upload-at')}</div>
<div className="col-8">{texture.upload_at}</div>
</div>
</div>
</div>
</div>
{canEdit && (
<div className="card card-warning">
<div className="card-header">
<h3 className="card-title">{t('admin.operationsTitle')}</h3>
</div>
<div className="card-body">
<p>{t('skinlib.show.manage-notice')}</p>
</div>
<div className="card-footer">
<div className="container d-flex justify-content-between">
<button className="btn btn-warning" onClick={handlePrivacyClick}>
{texture.public
? t('skinlib.setAsPrivate')
: t('skinlib.setAsPublic')}
</button>
<button
className="btn btn-danger"
onClick={handleDeleteTextureClick}
>
{t('skinlib.show.delete-texture')}
</button>
</div>
</div>
</div>
)}
<ModalApply
show={showModalApply}
canAdd={false}
{...{ [texture.type === 'cape' ? 'cape' : 'skin']: texture.tid }}
onClose={handleCloseModalApply}
/>
</>
)
}
export default hot(Show)

View File

@ -0,0 +1,5 @@
@use '../../../styles/utils';
.truncate {
@include utils.truncate-text;
}

View File

@ -1,8 +1,7 @@
import React from 'react'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { showModal, toast } from '@/scripts/notify'
import { ClosetItem } from '@/scripts/types'
import setAsAvatar from './setAsAvatar'
import styles from './ClosetItem.module.scss'
interface Props {
@ -20,29 +19,7 @@ const ClosetItem: React.FC<Props> = props => {
props.onClick(item)
}
const setAsAvatar = async () => {
try {
await showModal({
title: t('user.setAvatar'),
text: t('user.setAvatarNotice'),
})
} catch {
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/user/profile/avatar',
{ tid: item.tid },
)
if (code === 0) {
toast.success(message)
document
.querySelectorAll<HTMLImageElement>('[alt="User Image"]')
.forEach(el => (el.src += `?${new Date().getTime()}`))
} else {
toast.error(message)
}
}
const handleSetAsAvatar = () => setAsAvatar(item.tid)
return (
<div
@ -84,7 +61,7 @@ const ClosetItem: React.FC<Props> = props => {
>
{t('user.viewInSkinlib')}
</a>
<a href="#" className="dropdown-item" onClick={setAsAvatar}>
<a href="#" className="dropdown-item" onClick={handleSetAsAvatar}>
{t('user.setAsAvatar')}
</a>
</div>

View File

@ -10,6 +10,7 @@ import Pagination from '@/components/Pagination'
import ClosetItem from './ClosetItem'
import Previewer from './Previewer'
import ModalApply from './ModalApply'
import removeClosetItem from './removeClosetItem'
type Category = 'skin' | 'cape'
@ -125,24 +126,10 @@ const Closet: React.FC = () => {
}
const removeItem = async (item: Item) => {
try {
await showModal({
text: t('user.removeFromClosetNotice'),
okButtonType: 'danger',
})
} catch {
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
`/user/closet/remove/${item.tid}`,
)
if (code === 0) {
toast.success(message)
const { tid } = item
const { tid } = item
const ok = await removeClosetItem(tid)
if (ok) {
setItems(items => items.filter(item => item.tid !== tid))
} else {
toast.error(message)
}
}

View File

@ -0,0 +1,25 @@
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { showModal, toast } from '@/scripts/notify'
export default async function removeClosetItem(tid: number): Promise<boolean> {
try {
await showModal({
text: t('user.removeFromClosetNotice'),
okButtonType: 'danger',
})
} catch {
return false
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
`/user/closet/remove/${tid}`,
)
if (code === 0) {
toast.success(message)
} else {
toast.error(message)
}
return code === 0
}

View File

@ -0,0 +1,27 @@
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { showModal, toast } from '@/scripts/notify'
export default async function setAsAvatar(tid: number) {
try {
await showModal({
title: t('user.setAvatar'),
text: t('user.setAvatarNotice'),
})
} catch {
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/user/profile/avatar',
{ tid },
)
if (code === 0) {
toast.success(message)
document
.querySelectorAll<HTMLImageElement>('[alt="User Image"]')
.forEach(el => (el.src += `?${new Date().getTime()}`))
} else {
toast.error(message)
}
}

View File

@ -1,62 +0,0 @@
import 'bootstrap'
import Vue from 'vue'
import { mount } from '@vue/test-utils'
import { flushPromises } from '../utils'
import { trans } from '@/scripts/i18n'
import { toast } from '@/scripts/notify'
import ApplyToPlayerDialog from '@/components/ApplyToPlayerDialog.vue'
jest.mock('@/scripts/notify')
test('submit applying texture', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [{ pid: 1, name: 'a' }] })
Vue.prototype.$http.post.mockResolvedValueOnce({ code: 1 })
.mockResolvedValue({ code: 0, message: 'ok' })
const wrapper = mount<Vue & { fetchList(): Promise<void> }>(ApplyToPlayerDialog)
await wrapper.vm.fetchList()
const button = wrapper.find('.btn-outline-info')
button.trigger('click')
expect(toast.info).toBeCalledWith(trans('user.emptySelectedTexture'))
wrapper.setProps({ skin: 1 })
button.trigger('click')
expect(Vue.prototype.$http.post).toBeCalledWith(
'/user/player/set/1',
{
skin: 1,
cape: undefined,
},
)
wrapper.setProps({ skin: 0, cape: 1 })
button.trigger('click')
expect(Vue.prototype.$http.post).toBeCalledWith(
'/user/player/set/1',
{
skin: undefined,
cape: 1,
},
)
await flushPromises()
expect(toast.success).toBeCalledWith('ok')
})
test('compute avatar URL', () => {
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
// eslint-disable-next-line camelcase
const wrapper = mount<Vue & { avatarUrl(player: { tid_skin: number }): string }>(ApplyToPlayerDialog)
const { avatarUrl } = wrapper.vm
expect(avatarUrl({ tid_skin: 1 })).toBe('/avatar/1?3d&size=45')
})
test('search players', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [{ pid: 1, name: 'abc' }] })
const wrapper = mount<Vue & { fetchList(): Promise<void> }>(ApplyToPlayerDialog)
await wrapper.vm.fetchList()
wrapper.find('input').setValue('e')
expect(wrapper.find('.btn-outline-info').exists()).toBeFalse()
wrapper.find('input').setValue('b')
expect(wrapper.find('.btn-outline-info').exists()).toBeTrue()
})

View File

@ -1,64 +0,0 @@
import { mount } from '@vue/test-utils'
import Portal from '@/components/Portal'
beforeEach(() => {
document.body.innerHTML = ''
})
test('render default slot', () => {
mount(Portal, {
propsData: {
selector: 'body',
},
slots: {
default: 'children',
},
})
expect(document.querySelectorAll('div')).toHaveLength(1)
expect(document.querySelector('div')!.textContent).toBe('children')
})
test('custom wrapper tag', () => {
mount(Portal, {
propsData: {
selector: 'body',
tag: 'span',
},
})
expect(document.querySelectorAll('span')).toHaveLength(1)
})
test('disable portal', () => {
document.body.innerHTML = '<div id="c"></div>'
mount(Portal, {
propsData: {
selector: '#c',
disabled: true,
},
slots: {
default: 'children',
},
})
expect(document.querySelector('#c')!.innerHTML).toBe('')
})
test('should pass if container does not exist', () => {
mount(Portal, {
propsData: {
selector: '#nope',
},
})
})
test('replace container content', () => {
document.body.innerHTML = '<div>before</div>'
mount(Portal, {
propsData: {
selector: 'body',
},
slots: {
default: 'after',
},
})
expect(document.querySelector('div')!.textContent).toBe('after')
})

View File

@ -1,118 +0,0 @@
import Vue from 'vue'
import { mount } from '@vue/test-utils'
import Previewer from '@/components/Previewer.vue'
import * as emitter from '@/scripts/event'
import * as mockedSkinview3d from '../__mocks__/skinview3d'
type Viewer = Vue & { viewer: mockedSkinview3d.SkinViewer }
interface Handles {
handles: {
run: { paused: boolean }
walk: { paused: boolean }
rotate: { paused: boolean }
}
}
test('initialize skinview3d', () => {
const stub = jest.fn()
emitter.on('skinViewerMounted', stub)
const wrapper = mount<Viewer>(Previewer)
expect(wrapper.vm.viewer).toBeInstanceOf(mockedSkinview3d.SkinViewer)
expect(wrapper.vm.viewer.camera.position.z).toBe(70)
expect(stub).toBeCalledWith(expect.any(HTMLElement))
})
test('dispose viewer before destroy', () => {
const wrapper = mount<Viewer>(Previewer)
wrapper.destroy()
expect(wrapper.vm.viewer.disposed).toBeTrue()
})
test('skin URL should be updated', () => {
const wrapper = mount<Viewer>(Previewer)
wrapper.setProps({ skin: 'abc' })
expect(wrapper.vm.viewer.skinUrl).toBe('abc')
})
test('cape URL should be updated', () => {
const wrapper = mount<Viewer>(Previewer)
wrapper.setProps({ cape: 'abc' })
expect(wrapper.vm.viewer.capeUrl).toBe('abc')
})
test('`footer` slot', () => {
const wrapper = mount(Previewer, {
slots: {
footer: '<div id="footer" />',
},
})
expect(wrapper.find('#footer').exists()).toBeTrue()
})
test('disable closet mode', () => {
const wrapper = mount(Previewer)
expect(wrapper.find('.badge').text()).toBe('')
})
test('enable closet mode', () => {
const wrapper = mount(Previewer, {
propsData: {
closetMode: true,
},
})
expect(wrapper.find('.badge').text()).toBe('')
wrapper.setProps({ skin: 'abc' })
expect(wrapper.find('.badge').text()).toBe('general.skin')
wrapper.setProps({ cape: 'abc', skin: '' })
expect(wrapper.find('.badge').text()).toBe('general.cape')
wrapper.setProps({ skin: 'abc', cape: 'abc' })
expect(wrapper.find('.badge').text()).toBe('general.skin & general.cape')
})
test('toggle pause', () => {
const wrapper = mount(Previewer)
const pauseButton = wrapper.find('.fa-pause')
expect(pauseButton.exists()).toBeTrue()
pauseButton.trigger('click')
expect(wrapper.find('.fa-play').exists()).toBeTrue()
expect(wrapper.find('.fa-pause').exists()).toBeFalse()
})
test('toggle run', () => {
const wrapper = mount<Vue & Handles>(Previewer)
wrapper.find('.fa-forward').trigger('click')
expect(wrapper.vm.handles.run.paused).toBeFalse()
expect(wrapper.vm.handles.walk.paused).toBeFalse()
})
test('toggle rotate', () => {
const wrapper = mount<Vue & Handles>(Previewer)
wrapper.find('.fa-redo-alt').trigger('click')
expect(wrapper.vm.handles.rotate.paused).toBeTrue()
})
test('reset', () => {
mockedSkinview3d.SkinViewer.prototype.dispose = jest.fn(
function (this: mockedSkinview3d.SkinViewer) {
this.disposed = true
}.bind(new mockedSkinview3d.SkinViewer()),
)
const wrapper = mount<Viewer>(Previewer, {
propsData: {
model: 'alex',
},
})
wrapper.find('.fa-stop').trigger('click')
expect(mockedSkinview3d.SkinViewer.prototype.dispose).toBeCalled()
expect(wrapper.vm.viewer.playerObject.skin.slim).toBeTrue()
})
test('custom title', () => {
const wrapper = mount(Previewer, { propsData: { title: 'custom-title' } })
expect(wrapper.text()).toContain('custom-title')
})

View File

@ -12,7 +12,7 @@ interface Window {
site_name: string
version: string
i18n: object
extra: object
extra: any
}
fetch: jest.Mock

View File

@ -1,489 +0,0 @@
import Vue from 'vue'
import { mount } from '@vue/test-utils'
import { flushPromises } from '../../utils'
import { trans } from '@/scripts/i18n'
import { showModal, toast } from '@/scripts/notify'
import Show from '@/views/skinlib/Show.vue'
jest.mock('@/scripts/notify')
type Component = Vue & {
liked: boolean
likes: number
public: boolean
name: string
type: 'steve' | 'alex' | 'cape'
}
beforeEach(() => {
window.blessing.extra = {
download: true,
currentUid: 0,
admin: false,
nickname: 'author',
inCloset: false,
badges: [],
}
})
const previewer = Vue.extend({
render(h) {
return h('div', this.$slots.footer)
},
})
test('button for adding to closet should be disabled if not auth', () => {
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
expect(wrapper.find('.btn').attributes('disabled')).toBe('disabled')
})
test('button for adding to closet should be enabled if auth', () => {
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
Object.assign(window.blessing.extra, { inCloset: true, currentUid: 1 })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
expect(wrapper.find('[data-test="removeFromCloset"]').exists()).toBeTrue()
})
test('likes count indicator', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: { likes: 2 } })
Object.assign(window.blessing.extra, { inCloset: true, currentUid: 1 })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
await flushPromises()
expect(wrapper.find('.likes').classes()).toContain('text-red')
expect(wrapper.find('.likes').text()).toContain('2')
})
test('render basic information', async () => {
Vue.prototype.$http.get.mockResolvedValue({
data: {
name: 'my-texture',
type: 'alex',
hash: '123',
size: 2,
upload_at: '2018',
},
})
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
await flushPromises()
const text = wrapper.find('.card-primary').text()
expect(text).toContain('my-texture')
expect(text).toContain('alex')
expect(text).toContain('123...')
expect(text).toContain('2 KB')
expect(text).toContain('2018')
expect(text).toContain('author')
})
test('render action text of editing texture name', async () => {
Object.assign(window.blessing.extra, { admin: true })
Vue.prototype.$http.get.mockResolvedValue({ data: { uploader: 1, name: 'name' } })
let wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
await flushPromises()
expect(wrapper.contains('small')).toBeTrue()
Object.assign(window.blessing.extra, { currentUid: 2, admin: false })
wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
await flushPromises()
expect(wrapper.contains('small')).toBeFalse()
})
test('render nickname of uploader', () => {
Object.assign(window.blessing.extra, { nickname: null })
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
expect(wrapper.text()).toContain('general.unexistent-user')
})
test('operation panel should not be rendered if user is anonymous', async () => {
Object.assign(window.blessing.extra, { currentUid: 0 })
Vue.prototype.$http.get.mockResolvedValue({ data: { uploader: 1 } })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
await flushPromises()
expect(wrapper.find('.card-warning').exists()).toBeFalse()
})
test('operation panel should not be rendered if not privileged', async () => {
Object.assign(window.blessing.extra, { currentUid: 2 })
Vue.prototype.$http.get.mockResolvedValue({ data: { uploader: 1 } })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
await flushPromises()
expect(wrapper.find('.card-warning').exists()).toBeFalse()
})
test('operation panel should be rendered if privileged', async () => {
Object.assign(window.blessing.extra, { currentUid: 1 })
Vue.prototype.$http.get.mockResolvedValue({ data: { uploader: 1 } })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
await flushPromises()
expect(wrapper.find('.card-warning').exists()).toBeTrue()
})
test('download texture', async () => {
Object.assign(window.blessing.extra, { currentUid: 1 })
Vue.prototype.$http.get.mockResolvedValue({ data: { tid: 1, name: 'abc' } })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
await flushPromises()
wrapper.find('[data-test="download"]').trigger('click')
})
test('link to downloading texture', async () => {
Object.assign(window.blessing.extra, { download: false })
Vue.prototype.$http.get.mockResolvedValue({ data: { name: '', hash: '123' } })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
await flushPromises()
expect(wrapper.find('span[title="123"]').exists()).toBeTrue()
})
test('set as avatar', async () => {
Object.assign(window.blessing.extra, { currentUid: 1, inCloset: true })
Vue.prototype.$http.get.mockResolvedValueOnce({ data: { type: 'steve' } })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
await flushPromises()
wrapper.find('[data-test="setAsAvatar"]').trigger('click')
expect(showModal).toBeCalled()
})
test('hide "set avatar" button when texture is cape', async () => {
Vue.prototype.$http.get.mockResolvedValueOnce({ data: { type: 'cape' } })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
await flushPromises()
expect(wrapper.find('[data-test="setAsAvatar"]').exists()).toBeFalse()
})
test('add to closet', async () => {
Object.assign(window.blessing.extra, { currentUid: 1, inCloset: false })
Vue.prototype.$http.get.mockResolvedValue({ data: { name: 'wow', likes: 2 } })
Vue.prototype.$http.post.mockResolvedValue({ code: 0, message: '' })
showModal.mockResolvedValue({ value: 'a' })
const wrapper = mount<Component>(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
wrapper.find('[data-test="addToCloset"]').trigger('click')
await flushPromises()
expect(wrapper.vm.likes).toBe(3)
expect(wrapper.vm.liked).toBeTrue()
})
test('remove from closet', async () => {
Object.assign(window.blessing.extra, { currentUid: 1, inCloset: true })
Vue.prototype.$http.get.mockResolvedValue({ data: { likes: 2 } })
Vue.prototype.$http.post.mockResolvedValue({ code: 0 })
const wrapper = mount<Component>(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
wrapper.find('[data-test="removeFromCloset"]').trigger('click')
await flushPromises()
expect(wrapper.vm.likes).toBe(1)
expect(wrapper.vm.liked).toBeFalse()
})
test('change texture name', async () => {
Object.assign(window.blessing.extra, { admin: true })
Vue.prototype.$http.get.mockResolvedValue({ data: { name: 'old-name' } })
Vue.prototype.$http.post
.mockResolvedValueOnce({ code: 1, message: '1' })
.mockResolvedValue({ code: 0, message: '0' })
showModal
.mockRejectedValueOnce(null)
.mockResolvedValue({ value: 'new-name' })
const wrapper = mount<Component>(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
const button = wrapper.find('small > a')
button.trigger('click')
expect(Vue.prototype.$http.post).not.toBeCalled()
button.trigger('click')
await flushPromises()
expect(Vue.prototype.$http.post).toBeCalledWith(
'/skinlib/rename',
{ tid: 1, new_name: 'new-name' },
)
expect(toast.error).toBeCalledWith('1')
button.trigger('click')
await flushPromises()
expect(wrapper.vm.name).toBe('new-name')
})
test('change texture model', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: { type: 'steve' } })
Vue.prototype.$http.post
.mockResolvedValueOnce({ code: 1, message: '1' })
.mockResolvedValue({ code: 0, message: '0' })
const wrapper = mount<Component>(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
const modal = wrapper.find('#modal-type')
const button = wrapper
.findAll('small')
.at(1)
.find('a')
button.trigger('click')
wrapper
.findAll('[type=radio]')
.at(1)
.setChecked()
modal.vm.$emit('confirm')
await flushPromises()
expect(Vue.prototype.$http.post).toBeCalledWith(
'/skinlib/model',
{ tid: 1, model: 'alex' },
)
expect(toast.error).toBeCalledWith('1')
button.trigger('click')
wrapper
.findAll('[type=radio]')
.at(1)
.setChecked()
modal.vm.$emit('confirm')
await flushPromises()
expect(wrapper.vm.type).toBe('alex')
})
test('toggle privacy', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: { public: true } })
Vue.prototype.$http.post
.mockResolvedValueOnce({ code: 1, message: '1' })
.mockResolvedValue({ code: 0, message: '0' })
showModal
.mockRejectedValueOnce(null)
.mockResolvedValue({ value: '' })
const wrapper = mount<Component>(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
const button = wrapper.find('.btn-warning')
button.trigger('click')
expect(Vue.prototype.$http.post).not.toBeCalled()
button.trigger('click')
await flushPromises()
expect(Vue.prototype.$http.post).toBeCalledWith(
'/skinlib/privacy',
{ tid: 1 },
)
expect(toast.error).toBeCalledWith('1')
button.trigger('click')
await flushPromises()
expect(wrapper.vm.public).toBeFalse()
button.trigger('click')
await flushPromises()
expect(wrapper.vm.public).toBeTrue()
})
test('delete texture', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
Vue.prototype.$http.post
.mockResolvedValueOnce({ code: 1, message: '1' })
.mockResolvedValue({ code: 0, message: '0' })
showModal
.mockRejectedValueOnce(null)
.mockResolvedValue({ value: '' })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
const button = wrapper.find('.btn-danger')
button.trigger('click')
expect(Vue.prototype.$http.post).not.toBeCalled()
button.trigger('click')
await flushPromises()
expect(Vue.prototype.$http.post).toBeCalledWith(
'/skinlib/delete',
{ tid: 1 },
)
expect(toast.error).toBeCalledWith('1')
button.trigger('click')
await flushPromises()
jest.runAllTimers()
expect(toast.success).toBeCalledWith('0')
})
test('report texture', async () => {
Object.assign(window.blessing.extra, { currentUid: 1 })
Vue.prototype.$http.get.mockResolvedValue({ data: { report: 0 } })
Vue.prototype.$http.post
.mockResolvedValueOnce({ code: 1, message: 'duplicated' })
.mockResolvedValue({ code: 0, message: 'success' })
showModal
.mockRejectedValueOnce(null)
.mockRejectedValueOnce(null)
.mockResolvedValue({ value: 'reason' })
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
const button = wrapper.find('[data-test=report]')
button.trigger('click')
expect(showModal).toBeCalledWith({
mode: 'prompt',
title: trans('skinlib.report.title'),
text: '',
placeholder: trans('skinlib.report.reason'),
})
expect(Vue.prototype.$http.post).not.toBeCalled()
wrapper.setData({ reportScore: -5 })
button.trigger('click')
expect(showModal).toBeCalledWith({
mode: 'prompt',
title: trans('skinlib.report.title'),
text: trans('skinlib.report.negative', { score: 5 }),
placeholder: trans('skinlib.report.reason'),
})
wrapper.setData({ reportScore: 5 })
button.trigger('click')
expect(showModal).toBeCalledWith({
mode: 'prompt',
title: trans('skinlib.report.title'),
text: trans('skinlib.report.positive', { score: 5 }),
placeholder: trans('skinlib.report.reason'),
})
await flushPromises()
expect(Vue.prototype.$http.post).toBeCalledWith(
'/skinlib/report',
{ tid: 1, reason: 'reason' },
)
expect(toast.error).toBeCalledWith('duplicated')
button.trigger('click')
await flushPromises()
expect(toast.success).toBeCalledWith('success')
})
test('apply texture to player', () => {
Object.assign(window.blessing.extra, { currentUid: 1, inCloset: true })
Vue.prototype.$http.get
.mockResolvedValue({ data: {} })
.mockResolvedValue([])
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
stubs: { previewer },
})
wrapper.find('[data-target="#modal-use-as"]').trigger('click')
expect(wrapper.find('[data-target="#modal-add-player"]').exists()).toBeFalse()
})
test('truncate too long texture name', async () => {
Vue.prototype.$http.get.mockResolvedValue({
data: {
name: 'very-very-long-texture-name',
},
})
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
await flushPromises()
expect(wrapper.find('.card-primary').text()).toContain('very-very-long-...')
})
test('render badges', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
Object.assign(window.blessing.extra, {
badges: [{ text: 'reina', color: 'purple' }],
})
const wrapper = mount(Show, {
mocks: {
$route: ['/skinlib/show/1', '1'],
},
})
await flushPromises()
expect(wrapper.find('.badge.bg-purple').text()).toBe('reina')
})

View File

@ -0,0 +1,649 @@
import React from 'react'
import { render, fireEvent, wait } from '@testing-library/react'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { Texture } from '@/scripts/types'
import Show, { Badge } from '@/views/skinlib/Show'
jest.mock('@/scripts/net')
const fixtureSkin: Readonly<Texture> = Object.freeze<Texture>({
tid: 1,
name: 'skin',
type: 'steve',
hash: 'abc',
size: 2,
uploader: 1,
public: true,
upload_at: new Date().toString(),
likes: 1,
})
const fixtureCape: Readonly<Texture> = Object.freeze<Texture>({
tid: 2,
name: 'cape',
type: 'cape',
hash: 'def',
size: 2,
uploader: 1,
public: true,
upload_at: new Date().toString(),
likes: 1,
})
beforeEach(() => {
const container = document.createElement('div')
container.id = 'previewer'
document.body.appendChild(container)
window.blessing.extra = {
download: true,
currentUid: 0,
admin: false,
nickname: 'author',
inCloset: false,
report: 0,
badges: [],
}
})
afterEach(() => {
document.querySelector('#previewer')!.remove()
})
test('without authenticated', async () => {
fetch.get.mockResolvedValue({ data: fixtureSkin })
const { queryByText, queryByTitle } = render(<Show />)
await wait()
expect(queryByText(fixtureSkin.name)).toBeInTheDocument()
expect(queryByText('steve')).toBeInTheDocument()
expect(queryByText(`${fixtureSkin.size} KB`)).toBeInTheDocument()
expect(queryByText(fixtureSkin.hash)).toBeInTheDocument()
expect(queryByText(window.blessing.extra.nickname)).toHaveAttribute(
'href',
`/skinlib?filter=skin&uploader=${fixtureSkin.uploader}`,
)
expect(queryByTitle(t('skinlib.show.edit'))).not.toBeInTheDocument()
expect(queryByText(t('skinlib.addToCloset'))).toBeDisabled()
})
test('authenticated but not uploader', async () => {
fetch.get.mockResolvedValue({ data: fixtureCape })
const { queryByText, queryByTitle } = render(<Show />)
await wait()
expect(queryByText(fixtureCape.name)).toBeInTheDocument()
expect(queryByText(t('general.cape'))).toBeInTheDocument()
expect(queryByText(`${fixtureCape.size} KB`)).toBeInTheDocument()
expect(queryByText(fixtureCape.hash)).toBeInTheDocument()
expect(queryByText(window.blessing.extra.nickname)).toHaveAttribute(
'href',
`/skinlib?filter=cape&uploader=${fixtureCape.uploader}`,
)
expect(queryByTitle(t('skinlib.show.edit'))).not.toBeInTheDocument()
expect(queryByText(t('user.setAsAvatar'))).not.toBeInTheDocument()
})
test('uploader is not existed', async () => {
window.blessing.extra.nickname = null
fetch.get.mockResolvedValue({ data: fixtureSkin })
const { queryByText } = render(<Show />)
await wait()
expect(queryByText(t('general.unexistent-user'))).toBeInTheDocument()
})
test('badges', async () => {
window.blessing.extra.badges = [
{ text: 'STAFF', color: 'primary' },
] as Badge[]
fetch.get.mockResolvedValue({ data: fixtureSkin })
const { queryByText } = render(<Show />)
await wait()
expect(queryByText('STAFF')).toBeInTheDocument()
})
test('apply to player', async () => {
window.blessing.extra.currentUid = 2
window.blessing.extra.inCloset = true
fetch.get
.mockResolvedValueOnce({ data: fixtureSkin })
.mockResolvedValueOnce({ data: [] })
const { getByText, getByLabelText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.apply')))
fireEvent.click(getByLabelText('Close'))
expect(fetch.get).toBeCalledTimes(2)
})
test('set as avatar', async () => {
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
fetch.get.mockResolvedValue({ data: fixtureSkin })
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const { getByText, getByRole, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('user.setAsAvatar')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
})
describe('download texture', () => {
beforeEach(() => {
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
fetch.get.mockResolvedValue({ data: fixtureSkin })
})
it('allowed', async () => {
const { getByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.show.download')))
})
it('not allowed', async () => {
window.blessing.extra.download = false
const { queryByText } = render(<Show />)
await wait()
expect(queryByText(t('skinlib.show.download'))).not.toBeInTheDocument()
})
})
describe('operation panel', () => {
beforeEach(() => {
fetch.get.mockResolvedValue({ data: fixtureSkin })
})
it('uploader', async () => {
window.blessing.extra.currentUid = fixtureSkin.uploader
const { queryByText } = render(<Show />)
await wait()
expect(queryByText(t('skinlib.show.manage-notice'))).toBeInTheDocument()
})
it('administrator', async () => {
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
window.blessing.extra.admin = true
const { queryByText } = render(<Show />)
await wait()
expect(queryByText(t('skinlib.show.manage-notice'))).toBeInTheDocument()
})
})
describe('edit texture name', () => {
beforeEach(() => {
window.blessing.extra.currentUid = fixtureSkin.uploader
fetch.get.mockResolvedValue({ data: fixtureSkin })
})
it('cancelled', async () => {
const { getByText, getAllByTitle, getByDisplayValue, queryByText } = render(
<Show />,
)
await wait()
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[0])
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
target: { value: '' },
})
fireEvent.click(getByText(t('general.confirm')))
expect(queryByText(t('skinlib.emptyNewTextureName'))).toBeInTheDocument()
fireEvent.click(getByText(t('general.cancel')))
await wait()
expect(fetch.post).not.toBeCalled()
expect(queryByText(fixtureSkin.name)).toBeInTheDocument()
})
it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const {
getByText,
getAllByTitle,
getByDisplayValue,
getByRole,
queryByText,
} = render(<Show />)
await wait()
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[0])
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
target: { value: 't' },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/skinlib/rename', {
tid: fixtureSkin.tid,
new_name: 't',
})
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
expect(queryByText('t')).toBeInTheDocument()
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const {
getByText,
getAllByTitle,
getByDisplayValue,
getByRole,
queryByText,
} = render(<Show />)
await wait()
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[0])
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
target: { value: 't' },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/skinlib/rename', {
tid: fixtureSkin.tid,
new_name: 't',
})
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
expect(queryByText(fixtureSkin.name)).toBeInTheDocument()
})
})
describe('edit texture type', () => {
beforeEach(() => {
Object.assign(window.blessing.extra, { currentUid: fixtureSkin.uploader })
fetch.get.mockResolvedValue({ data: fixtureSkin })
})
it('cancelled', async () => {
const { getByText, getAllByTitle, getByLabelText, queryByText } = render(
<Show />,
)
await wait()
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[1])
fireEvent.click(getByLabelText('Alex'))
fireEvent.click(getByText(t('general.cancel')))
await wait()
expect(fetch.post).not.toBeCalled()
expect(queryByText('steve')).toBeInTheDocument()
})
it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const {
getByText,
getAllByTitle,
getByLabelText,
getByRole,
queryByText,
} = render(<Show />)
await wait()
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[1])
fireEvent.click(getByLabelText('Alex'))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/skinlib/model', {
tid: fixtureSkin.tid,
model: 'alex',
})
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
expect(queryByText('alex')).toBeInTheDocument()
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const {
getByText,
getAllByTitle,
getByLabelText,
getByRole,
queryByText,
} = render(<Show />)
await wait()
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[1])
fireEvent.click(getByLabelText('Alex'))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/skinlib/model', {
tid: fixtureSkin.tid,
model: 'alex',
})
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
expect(queryByText('steve')).toBeInTheDocument()
})
})
describe('add to closet', () => {
beforeEach(() => {
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
fetch.get.mockResolvedValue({ data: fixtureSkin })
})
it('cancelled', async () => {
const { getByText, getByDisplayValue, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.addToCloset')))
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
target: { value: '' },
})
fireEvent.click(getByText(t('general.confirm')))
expect(queryByText(t('skinlib.emptyItemName'))).toBeInTheDocument()
fireEvent.click(getByText(t('general.cancel')))
await wait()
expect(fetch.post).not.toBeCalled()
})
it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const { getByText, getByDisplayValue, getByRole, queryByText } = render(
<Show />,
)
await wait()
fireEvent.click(getByText(t('skinlib.addToCloset')))
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
target: { value: 't' },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/user/closet/add', {
tid: fixtureSkin.tid,
name: 't',
})
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
expect(queryByText('2')).toBeInTheDocument()
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByDisplayValue, getByRole, queryByText } = render(
<Show />,
)
await wait()
fireEvent.click(getByText(t('skinlib.addToCloset')))
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
target: { value: 't' },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/user/closet/add', {
tid: fixtureSkin.tid,
name: 't',
})
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
expect(queryByText('1')).toBeInTheDocument()
})
})
describe('remove from closet', () => {
beforeEach(() => {
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
window.blessing.extra.inCloset = true
fetch.get.mockResolvedValue({ data: fixtureSkin })
})
it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const { getByText, getByRole, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.removeFromCloset')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith(`/user/closet/remove/${fixtureSkin.tid}`)
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
expect(queryByText('0')).toBeInTheDocument()
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByRole, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.removeFromCloset')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith(`/user/closet/remove/${fixtureSkin.tid}`)
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
expect(queryByText('1')).toBeInTheDocument()
})
})
describe('report texture', () => {
beforeEach(() => {
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
fetch.get.mockResolvedValue({ data: fixtureSkin })
})
it('positive score', async () => {
window.blessing.extra.report = 5
const { getByText, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.report.title')))
expect(queryByText(t('skinlib.report.positive', { score: 5 })))
fireEvent.click(getByText(t('general.cancel')))
expect(fetch.post).not.toBeCalled()
})
it('negative score', async () => {
window.blessing.extra.report = -5
const { getByText, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.report.title')))
expect(queryByText(t('skinlib.report.negative', { score: 5 })))
fireEvent.click(getByText(t('general.cancel')))
expect(fetch.post).not.toBeCalled()
})
it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const { getByText, getByPlaceholderText, getByRole, queryByText } = render(
<Show />,
)
await wait()
fireEvent.click(getByText(t('skinlib.report.title')))
fireEvent.input(getByPlaceholderText(t('skinlib.report.reason')), {
target: { value: 'illegal' },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/skinlib/report', {
tid: fixtureSkin.tid,
reason: 'illegal',
})
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByPlaceholderText, getByRole, queryByText } = render(
<Show />,
)
await wait()
fireEvent.click(getByText(t('skinlib.report.title')))
fireEvent.input(getByPlaceholderText(t('skinlib.report.reason')), {
target: { value: 'illegal' },
})
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/skinlib/report', {
tid: fixtureSkin.tid,
reason: 'illegal',
})
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
})
})
describe('change privacy', () => {
beforeEach(() => {
window.blessing.extra.currentUid = fixtureSkin.uploader
})
it('cancelled', async () => {
fetch.get.mockResolvedValue({ data: fixtureSkin })
const { getByText, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.setAsPrivate')))
fireEvent.click(getByText(t('general.cancel')))
await wait()
expect(fetch.post).not.toBeCalled()
expect(queryByText(t('skinlib.setAsPrivate'))).toBeInTheDocument()
})
it('succeeded', async () => {
fetch.get.mockResolvedValue({ data: fixtureSkin })
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const { getByText, getByRole, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.setAsPrivate')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/skinlib/privacy', {
tid: fixtureSkin.tid,
})
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
expect(queryByText(t('skinlib.setAsPublic'))).toBeInTheDocument()
})
it('failed', async () => {
fetch.get.mockResolvedValue({ data: { ...fixtureSkin, public: false } })
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByRole, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.setAsPublic')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/skinlib/privacy', {
tid: fixtureSkin.tid,
})
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
expect(queryByText(t('skinlib.setAsPublic'))).toBeInTheDocument()
})
})
describe('delete texture', () => {
beforeEach(() => {
window.blessing.extra.currentUid = fixtureSkin.uploader
fetch.get.mockResolvedValue({ data: fixtureSkin })
})
it('cancelled', async () => {
const { getByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.show.delete-texture')))
fireEvent.click(getByText(t('general.cancel')))
await wait()
expect(fetch.post).not.toBeCalled()
})
it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const { getByText, getByRole, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.show.delete-texture')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/skinlib/delete', {
tid: fixtureSkin.tid,
})
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
jest.runAllTimers()
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByRole, queryByText } = render(<Show />)
await wait()
fireEvent.click(getByText(t('skinlib.show.delete-texture')))
fireEvent.click(getByText(t('general.confirm')))
await wait()
expect(fetch.post).toBeCalledWith('/skinlib/delete', {
tid: fixtureSkin.tid,
})
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
})
})

View File

@ -1 +0,0 @@
<div id="texture-info"></div>

View File

@ -1 +0,0 @@
<div id="operations"></div>

View File

@ -0,0 +1 @@
<div id="side"></div>