rewrite texture upload page
This commit is contained in:
parent
d4c3264324
commit
27e6042b93
|
|
@ -31,12 +31,12 @@
|
|||
"react-draggable": "^4.2.0",
|
||||
"react-hot-loader": "^4.12.18",
|
||||
"rxjs": "^6.5.3",
|
||||
"skinview-utils": "^0.2.1",
|
||||
"skinview3d": "^1.2.1",
|
||||
"spectre.css": "^0.5.8",
|
||||
"vue": "^2.6.11",
|
||||
"vue-good-table": "^2.18.1",
|
||||
"vue-recaptcha": "^1.2.0",
|
||||
"vue-upload-component": "^2.8.20",
|
||||
"vuejs-paginate": "^2.1.0",
|
||||
"xterm": "^4.4.0",
|
||||
"xterm-addon-fit": "^0.3.0"
|
||||
|
|
|
|||
11
resources/assets/src/scripts/hooks/useBlessingExtra.ts
Normal file
11
resources/assets/src/scripts/hooks/useBlessingExtra.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function useBlessingExtra<T>(key: string): T {
|
||||
const [value, setValue] = useState<T>({} as T)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(blessing.extra[key] as T)
|
||||
}, [])
|
||||
|
||||
return value
|
||||
}
|
||||
|
|
@ -123,7 +123,7 @@ export default [
|
|||
},
|
||||
{
|
||||
path: 'skinlib/upload',
|
||||
component: () => import('../views/skinlib/Upload.vue'),
|
||||
el: virtual,
|
||||
react: () => import('../views/skinlib/Upload'),
|
||||
el: '#file-input',
|
||||
},
|
||||
]
|
||||
|
|
|
|||
38
resources/assets/src/scripts/textureUtils.ts
Normal file
38
resources/assets/src/scripts/textureUtils.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { loadSkinToCanvas } from 'skinview-utils'
|
||||
|
||||
/* istanbul ignore next */
|
||||
function isTransparent(
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
): boolean {
|
||||
const imageData = context.getImageData(x, y, 1, 1)
|
||||
|
||||
return imageData.data[3] !== 255
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function isAlex(texture: string): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const image = new Image()
|
||||
image.src = texture
|
||||
image.onload = () => {
|
||||
if (image.width !== image.height) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
loadSkinToCanvas(canvas, image)
|
||||
|
||||
const ratio = canvas.width / 64
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
|
||||
resolve(isTransparent(context, 46 * ratio, 63 * ratio))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<portal selector="#file-input" :disabled="disablePortal">
|
||||
<div class="card card-primary">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label v-t="'skinlib.upload.texture-name'" for="name" />
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="textureNameRule"
|
||||
class="form-control"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label v-t="'skinlib.upload.texture-type'" />
|
||||
<br>
|
||||
<label class="mr-2">
|
||||
<input
|
||||
v-model="type"
|
||||
type="radio"
|
||||
name="type"
|
||||
value="steve"
|
||||
>
|
||||
Steve
|
||||
</label>
|
||||
<label class="mr-2">
|
||||
<input
|
||||
v-model="type"
|
||||
type="radio"
|
||||
name="type"
|
||||
value="alex"
|
||||
>
|
||||
Alex
|
||||
</label>
|
||||
<label class="mr-2">
|
||||
<input
|
||||
v-model="type"
|
||||
type="radio"
|
||||
name="type"
|
||||
value="cape"
|
||||
>
|
||||
{{ $t('general.cape') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label v-t="'skinlib.upload.select-file'" for="file" />
|
||||
<div class="file-dnd">
|
||||
<img v-if="hasFile" :src="texture" :width="width2d">
|
||||
<h3 v-else v-t="'skinlib.upload.dropZone'" />
|
||||
</div>
|
||||
<file-upload
|
||||
ref="upload"
|
||||
v-model="files"
|
||||
extensions="png"
|
||||
accept="image/png,image/x-png"
|
||||
drop=".file-dnd"
|
||||
@input-file="inputFile"
|
||||
>
|
||||
<button class="btn btn-primary">
|
||||
{{ $t('skinlib.upload.select-file') }}
|
||||
</button>
|
||||
</file-upload>
|
||||
<button
|
||||
v-show="hasFile"
|
||||
class="btn btn-danger float-right"
|
||||
data-test="remove"
|
||||
@click="remove"
|
||||
>
|
||||
<i class="fas fa-trash-alt" />
|
||||
{{ $t('skinlib.upload.remove') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="contentPolicy" class="callout callout-warning" v-html="contentPolicy" />
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="container pl-0 pr-0 d-flex justify-content-between">
|
||||
<label
|
||||
class="mt-2"
|
||||
:title="$t('skinlib.upload.privacy-notice')"
|
||||
data-toggle="tooltip"
|
||||
>
|
||||
<input v-model="isPrivate" type="checkbox">
|
||||
{{ $t('skinlib.upload.set-as-private') }}
|
||||
</label>
|
||||
<button v-if="uploading" class="btn btn-success" disabled>
|
||||
<i class="fa fa-spinner fa-spin" /> {{ $t('skinlib.uploading') }}
|
||||
</button>
|
||||
<button v-else class="btn btn-success" @click="upload">
|
||||
{{ $t('skinlib.upload.button') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="hasFile" class="callout callout-info bottom-notice">
|
||||
<p>{{ $t('skinlib.upload.cost', { score: scoreCost }) }}</p>
|
||||
</div>
|
||||
<div v-if="isPrivate" class="callout callout-info bottom-notice">
|
||||
<p>{{ privacyNotice }}</p>
|
||||
</div>
|
||||
<div v-if="!isPrivate && award" class="callout callout-success bottom-notice">
|
||||
<p>{{ $t('skinlib.upload.award', { score: award }) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</portal>
|
||||
|
||||
<portal selector="#previewer" :disabled="disablePortal">
|
||||
<previewer
|
||||
:skin="type !== 'cape' ? texture : ''"
|
||||
:cape="type === 'cape' ? texture : ''"
|
||||
:model="type"
|
||||
/>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FileUpload from 'vue-upload-component'
|
||||
import { isSlimSkin } from 'skinview3d'
|
||||
import Portal from '../../components/Portal'
|
||||
import emitMounted from '../../components/mixins/emitMounted'
|
||||
import { toast } from '../../scripts/notify'
|
||||
|
||||
export default {
|
||||
name: 'Upload',
|
||||
components: {
|
||||
FileUpload,
|
||||
Portal,
|
||||
Previewer: () => import('../../components/Previewer.vue'),
|
||||
},
|
||||
mixins: [
|
||||
emitMounted,
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
type: 'steve',
|
||||
isPrivate: false,
|
||||
files: [],
|
||||
texture: '',
|
||||
uploading: false,
|
||||
textureNameRule: blessing.extra.rule,
|
||||
privacyNotice: blessing.extra.privacyNotice,
|
||||
scorePublic: blessing.extra.scorePublic,
|
||||
scorePrivate: blessing.extra.scorePrivate,
|
||||
award: blessing.extra.award,
|
||||
contentPolicy: blessing.extra.contentPolicy,
|
||||
width2d: 64,
|
||||
disablePortal: process.env.NODE_ENV === 'test',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
scoreCost() {
|
||||
const size = Math.round(this.files[0].size / 1024) || 1
|
||||
return size * (this.isPrivate ? this.scorePrivate : this.scorePublic)
|
||||
},
|
||||
hasFile() {
|
||||
return this.files[0]
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async upload() {
|
||||
if (!this.hasFile) {
|
||||
toast.error(this.$t('skinlib.emptyUploadFile'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.name) {
|
||||
toast.error(this.$t('skinlib.emptyTextureName'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!/image\/(x-)?png/.test(this.files[0].type)) {
|
||||
toast.error(this.$t('skinlib.fileExtError'))
|
||||
return
|
||||
}
|
||||
|
||||
const data = new FormData()
|
||||
data.append('name', this.name)
|
||||
data.append('type', this.type)
|
||||
data.append('file', this.files[0].file, this.files[0].name)
|
||||
data.append('public', !this.isPrivate)
|
||||
|
||||
this.uploading = true
|
||||
const {
|
||||
code, message, data: { tid } = { tid: 0 },
|
||||
} = await this.$http.post('/skinlib/upload', data)
|
||||
if (code === 0) {
|
||||
window.location = `${blessing.base_url}/skinlib/show/${tid}`
|
||||
} else {
|
||||
toast.error(message)
|
||||
this.uploading = false
|
||||
}
|
||||
},
|
||||
inputFile(file) {
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.name) {
|
||||
const matched = /(.*)\.png$/i.exec(file.name)
|
||||
this.name = matched ? matched[1] : file.name
|
||||
}
|
||||
this.texture = URL.createObjectURL(file.file)
|
||||
|
||||
const image = new Image()
|
||||
image.src = this.texture
|
||||
image.onload = () => {
|
||||
this.width2d = image.width > 400 ? 400 : image.width
|
||||
this.type = isSlimSkin(image) ? 'alex' : 'steve'
|
||||
}
|
||||
},
|
||||
remove() {
|
||||
this.$refs.upload.clear()
|
||||
this.texture = ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.file-dnd
|
||||
min-height 256px
|
||||
margin-bottom 15px
|
||||
border 1px solid #ddd
|
||||
border-radius 2px
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
h3
|
||||
color #aaa
|
||||
|
||||
.bottom-notice
|
||||
margin-top 10px
|
||||
</style>
|
||||
277
resources/assets/src/views/skinlib/Upload/index.tsx
Normal file
277
resources/assets/src/views/skinlib/Upload/index.tsx
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { hot } from 'react-hot-loader/root'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { showModal, toast } from '@/scripts/notify'
|
||||
import { isAlex } from '@/scripts/textureUtils'
|
||||
import { TextureType } from '@/scripts/types'
|
||||
import ViewerSkeleton from '@/components/ViewerSkeleton'
|
||||
import styles from './styles.module.scss'
|
||||
|
||||
const Previewer = React.lazy(() => import('@/components/Viewer'))
|
||||
|
||||
const container = document.createElement('div')
|
||||
|
||||
const Upload: React.FC = () => {
|
||||
const [name, setName] = useState('')
|
||||
const [type, setType] = useState<TextureType>('steve')
|
||||
const [isPrivate, setIsPrivate] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [texture, setTexture] = useState('')
|
||||
const fileInput = useRef<HTMLInputElement>(null)
|
||||
const nameRule = useBlessingExtra<string>('rule')
|
||||
const contentPolicy = useBlessingExtra<string>('contentPolicy')
|
||||
const privacyNotice = useBlessingExtra<string>('privacyNotice')
|
||||
const award = useBlessingExtra<number>('award')
|
||||
const scorePublic = useBlessingExtra<number>('scorePublic')
|
||||
const scorePrivate = useBlessingExtra<number>('scorePrivate')
|
||||
|
||||
useEffect(() => {
|
||||
const mount = document.querySelector('#previewer')!
|
||||
mount.appendChild(container)
|
||||
|
||||
return () => {
|
||||
mount.removeChild(container)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value)
|
||||
}
|
||||
|
||||
const handleTypeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setType(event.target.value as TextureType)
|
||||
}
|
||||
|
||||
const handlePrivateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsPrivate(event.target.checked)
|
||||
}
|
||||
|
||||
const invokeSelectFile = () => {
|
||||
fileInput.current!.click()
|
||||
}
|
||||
|
||||
const handleSelectFile = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const files = event.target.files!
|
||||
const [file] = files
|
||||
if (file) {
|
||||
setFile(file)
|
||||
if (!name && file.name.endsWith('.png')) {
|
||||
setName(file.name.slice(0, file.name.length - 4))
|
||||
}
|
||||
const texture = URL.createObjectURL(file)
|
||||
setTexture(texture)
|
||||
if (type !== 'cape') {
|
||||
setType((await isAlex(texture)) ? 'alex' : 'steve')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!file) {
|
||||
toast.error(t('skinlib.emptyUploadFile'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
toast.error(t('skinlib.emptyTextureName'))
|
||||
return
|
||||
}
|
||||
|
||||
if (file.type !== 'image/png' && file.type !== 'image/x-png') {
|
||||
toast.error(t('skinlib.fileExtError'))
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('name', name)
|
||||
formData.append('type', type)
|
||||
formData.append('file', file, file.name)
|
||||
formData.append('public', (!isPrivate).toString())
|
||||
|
||||
setIsUploading(true)
|
||||
const { code, message, data: { tid } = { tid: 0 } } = await fetch.post<
|
||||
fetch.ResponseBody<{ tid: number }>
|
||||
>('/skinlib/upload', formData)
|
||||
setIsUploading(false)
|
||||
|
||||
if (code === 0) {
|
||||
window.location.href = `${blessing.base_url}/skinlib/show/${tid}`
|
||||
} else if (code === 2) {
|
||||
try {
|
||||
await showModal({
|
||||
mode: 'confirm',
|
||||
text: message,
|
||||
okButtonText: t('user.viewInSkinlib'),
|
||||
})
|
||||
window.location.href = `${blessing.base_url}/skinlib/show/${tid}`
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const costRatio = isPrivate ? scorePrivate : scorePublic
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card card-primary">
|
||||
<div className="card-body">
|
||||
<div className="form-group">
|
||||
<label htmlFor="texture-name">
|
||||
{t('skinlib.upload.texture-name')}
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="texture-name"
|
||||
type="text"
|
||||
placeholder={nameRule}
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>{t('skinlib.upload.texture-type')}</label>
|
||||
<br />
|
||||
<label className="mr-4">
|
||||
<input
|
||||
type="radio"
|
||||
className="mr-1"
|
||||
name="type"
|
||||
value="steve"
|
||||
checked={type === 'steve'}
|
||||
onChange={handleTypeChange}
|
||||
/>
|
||||
Steve
|
||||
</label>
|
||||
<label className="mr-4">
|
||||
<input
|
||||
type="radio"
|
||||
className="mr-1"
|
||||
name="type"
|
||||
value="alex"
|
||||
checked={type === 'alex'}
|
||||
onChange={handleTypeChange}
|
||||
/>
|
||||
Alex
|
||||
</label>
|
||||
<label className="mr-4">
|
||||
<input
|
||||
type="radio"
|
||||
className="mr-1"
|
||||
name="type"
|
||||
value="cape"
|
||||
checked={type === 'cape'}
|
||||
onChange={handleTypeChange}
|
||||
/>
|
||||
{t('general.cape')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="select-file">
|
||||
{t('skinlib.upload.select-file')}
|
||||
</label>
|
||||
<div className="input-group">
|
||||
<div className="custom-file">
|
||||
<input
|
||||
type="file"
|
||||
className="custom-file-input"
|
||||
id="select-file"
|
||||
accept="image/png, image/x-png"
|
||||
title={t('skinlib.upload.select-file')}
|
||||
ref={fileInput}
|
||||
onChange={handleSelectFile}
|
||||
/>
|
||||
<label className={`custom-file-label ${styles.label}`}>
|
||||
{file?.name}
|
||||
</label>
|
||||
</div>
|
||||
<div className="input-group-append">
|
||||
<button className="btn btn-default" onClick={invokeSelectFile}>
|
||||
{t('skinlib.upload.select-file')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{contentPolicy && (
|
||||
<div
|
||||
className="callout callout-warning"
|
||||
dangerouslySetInnerHTML={{ __html: contentPolicy }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-footer">
|
||||
<div className="container px-0 d-flex justify-content-between">
|
||||
<label
|
||||
className="mt-2"
|
||||
htmlFor="is-private"
|
||||
title={t('skinlib.upload.privacy-notice')}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="is-private"
|
||||
className="mr-1"
|
||||
checked={isPrivate}
|
||||
onChange={handlePrivateChange}
|
||||
/>
|
||||
{t('skinlib.upload.set-as-private')}
|
||||
</label>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
disabled={isUploading}
|
||||
onClick={handleUpload}
|
||||
>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<i className="fas fa-spinner fa-spin mr-1" />
|
||||
<span>{t('skinlib.uploading')}</span>
|
||||
</>
|
||||
) : (
|
||||
t('skinlib.upload.button')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{file && (
|
||||
<div className="callout callout-success mt-3">
|
||||
<p>
|
||||
{t('skinlib.upload.cost', {
|
||||
score: (~~(file.size / 1024) || 1) * costRatio,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isPrivate && (
|
||||
<div className="callout callout-info mt-3">
|
||||
<p>{privacyNotice}</p>
|
||||
</div>
|
||||
)}
|
||||
{!isPrivate && award > 0 && (
|
||||
<div className="callout callout-success mt-3">
|
||||
<p>{t('skinlib.upload.award', { score: award })}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{ReactDOM.createPortal(
|
||||
<React.Suspense fallback={<ViewerSkeleton />}>
|
||||
<Previewer
|
||||
skin={type !== 'cape' ? texture : undefined}
|
||||
cape={type === 'cape' ? texture : undefined}
|
||||
isAlex={type === 'alex'}
|
||||
/>
|
||||
</React.Suspense>,
|
||||
container,
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default hot(Upload)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.label::after {
|
||||
display: none;
|
||||
}
|
||||
4
resources/assets/tests/__mocks__/skinview-utils.ts
Normal file
4
resources/assets/tests/__mocks__/skinview-utils.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export function loadSkinToCanvas(
|
||||
_canvas: HTMLCanvasElement,
|
||||
_image: HTMLImageElement,
|
||||
) {}
|
||||
1
resources/assets/tests/ts-shims/textureUtils.ts
Normal file
1
resources/assets/tests/ts-shims/textureUtils.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const isAlex = {} as jest.Mock<Promise<boolean>, [string]>
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
/* eslint-disable accessor-pairs */
|
||||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import * as skinview3d from 'skinview3d'
|
||||
import { flushPromises } from '../../utils'
|
||||
import { toast } from '@/scripts/notify'
|
||||
import Upload from '@/views/skinlib/Upload.vue'
|
||||
|
||||
jest.mock('@/scripts/notify')
|
||||
|
||||
window.blessing.extra = {
|
||||
textureNameRule: 'rule',
|
||||
privacyNotice: 'privacyNotice',
|
||||
scorePrivate: 10,
|
||||
scorePublic: 1,
|
||||
award: 0,
|
||||
contentPolicy: 'the policy',
|
||||
}
|
||||
|
||||
const csrf = document.createElement('meta')
|
||||
csrf.name = 'csrf-token'
|
||||
csrf.content = 'token'
|
||||
document.head.appendChild(csrf)
|
||||
|
||||
test('display drap and drop notice', () => {
|
||||
const wrapper = mount(Upload, {
|
||||
stubs: ['file-upload'],
|
||||
})
|
||||
expect(wrapper.text()).toContain('skinlib.upload.dropZone')
|
||||
wrapper.setData({ files: [{}] })
|
||||
expect(wrapper.contains('img')).toBeTrue()
|
||||
})
|
||||
|
||||
test('button for removing texture', () => {
|
||||
const wrapper = mount<Vue & { texture: string }>(Upload, {
|
||||
stubs: ['file-upload'],
|
||||
})
|
||||
Object.defineProperty(wrapper.vm.$refs.upload, 'clear', {
|
||||
get: () => jest.fn(),
|
||||
})
|
||||
const button = wrapper.find('[data-test=remove]')
|
||||
expect(button.isVisible()).toBeFalse()
|
||||
wrapper.setData({ files: [{}] })
|
||||
expect(button.isVisible()).toBeTrue()
|
||||
button.trigger('click')
|
||||
expect(wrapper.vm.texture).toBe('')
|
||||
})
|
||||
|
||||
test('notice should be display if texture is private', () => {
|
||||
const wrapper = mount(Upload, {
|
||||
stubs: ['file-upload'],
|
||||
})
|
||||
expect(wrapper.contains('.callout-info')).toBeFalse()
|
||||
wrapper.find('[type=checkbox]').setChecked()
|
||||
expect(wrapper.find('.callout-info').text()).toBe('privacyNotice')
|
||||
})
|
||||
|
||||
test('display score cost', () => {
|
||||
const origin = Vue.prototype.$t
|
||||
Vue.prototype.$t = (key, args) => `${key}${JSON.stringify(args)}`
|
||||
|
||||
const wrapper = mount(Upload, {
|
||||
stubs: ['file-upload'],
|
||||
})
|
||||
wrapper.find('[type=checkbox]').setChecked()
|
||||
wrapper.setData({ files: [{ size: 1024 }] })
|
||||
expect(wrapper.text()).toContain(JSON.stringify({ score: 10 }))
|
||||
|
||||
Vue.prototype.$t = origin
|
||||
})
|
||||
|
||||
test('process input file', () => {
|
||||
window.URL.createObjectURL = jest.fn().mockReturnValue('file-url')
|
||||
;(window as Window & typeof globalThis & { Image: jest.Mock }).Image = jest.fn()
|
||||
.mockImplementationOnce(function (this: HTMLImageElement) {
|
||||
this.src = ''
|
||||
this.onload = null
|
||||
Object.defineProperty(this, 'onload', {
|
||||
set: fn => {
|
||||
fn()
|
||||
},
|
||||
})
|
||||
})
|
||||
.mockImplementationOnce(function (this: HTMLImageElement) {
|
||||
this.src = ''
|
||||
this.width = 500
|
||||
this.onload = null
|
||||
Object.defineProperty(this, 'onload', {
|
||||
set: fn => {
|
||||
fn()
|
||||
},
|
||||
})
|
||||
})
|
||||
jest.spyOn(skinview3d, 'isSlimSkin')
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValue(false)
|
||||
const blob = new Blob()
|
||||
type Component = Vue & {
|
||||
name: string
|
||||
texture: string
|
||||
type: string
|
||||
inputFile(attrs?: { file: Blob, name: string }): void
|
||||
}
|
||||
const wrapper = mount<Component>(Upload, {
|
||||
stubs: ['file-upload'],
|
||||
})
|
||||
|
||||
wrapper.vm.inputFile()
|
||||
expect(wrapper.vm.name).toBe('')
|
||||
|
||||
wrapper.vm.inputFile({ file: blob, name: '123.png' })
|
||||
expect(wrapper.vm.name).toBe('123')
|
||||
expect(wrapper.vm.type).toBe('alex')
|
||||
|
||||
wrapper.vm.inputFile({ file: blob, name: '456.png' })
|
||||
expect(wrapper.vm.name).toBe('123')
|
||||
expect(wrapper.vm.type).toBe('steve')
|
||||
|
||||
wrapper.setData({ name: '' })
|
||||
wrapper.vm.inputFile({ file: blob, name: '789' })
|
||||
expect(wrapper.vm.name).toBe('789')
|
||||
|
||||
expect(wrapper.vm.texture).toBe('file-url')
|
||||
|
||||
;(window as Window & typeof globalThis & { Image: jest.Mock }).Image.mockRestore()
|
||||
})
|
||||
|
||||
test('upload file', async () => {
|
||||
(window as Window & typeof globalThis & { Request: jest.Mock }).Request = jest.fn()
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValueOnce({
|
||||
code: 0, message: '0', data: { tid: 1 },
|
||||
})
|
||||
|
||||
const wrapper = mount(Upload, {
|
||||
stubs: ['file-upload'],
|
||||
})
|
||||
const button = wrapper.find('.btn-success')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
expect(toast.error).toBeCalledWith('skinlib.emptyUploadFile')
|
||||
|
||||
wrapper.setData({
|
||||
files: [{
|
||||
file: {}, name: 't', type: 'image/jpeg',
|
||||
}],
|
||||
})
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
expect(toast.error).toBeCalledWith('skinlib.emptyTextureName')
|
||||
|
||||
wrapper.find('[type=text]').setValue('t')
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
expect(toast.error).toBeCalledWith('skinlib.fileExtError')
|
||||
|
||||
wrapper.setData({
|
||||
files: [{
|
||||
file: new Blob(), name: 't.png', type: 'image/png',
|
||||
}],
|
||||
})
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith('/skinlib/upload', expect.any(FormData))
|
||||
expect(toast.error).toBeCalledWith('1')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
test('show notice about awarding', () => {
|
||||
const wrapper = mount(Upload)
|
||||
expect(wrapper.find('.callout-success').exists()).toBeFalse()
|
||||
|
||||
wrapper.setData({ award: 5 })
|
||||
expect(wrapper.find('.callout-success').exists()).toBeTrue()
|
||||
|
||||
wrapper.find('[type=checkbox]').setChecked()
|
||||
expect(wrapper.find('.callout-success').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('show content policy', () => {
|
||||
const wrapper = mount(Upload)
|
||||
expect(wrapper.find('.callout-warning').exists()).toBeTrue()
|
||||
})
|
||||
277
resources/assets/tests/views/skinlib/Upload.test.tsx
Normal file
277
resources/assets/tests/views/skinlib/Upload.test.tsx
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import React from 'react'
|
||||
import { render, fireEvent, wait } from '@testing-library/react'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { isAlex } from '@/scripts/textureUtils'
|
||||
import Upload from '@/views/skinlib/Upload'
|
||||
|
||||
jest.mock('@/scripts/net')
|
||||
jest.mock('@/scripts/textureUtils')
|
||||
|
||||
beforeEach(() => {
|
||||
const container = document.createElement('div')
|
||||
container.id = 'previewer'
|
||||
document.body.appendChild(container)
|
||||
|
||||
window.blessing.extra = {
|
||||
rule: 'rule',
|
||||
privacyNotice: 'privacy notice',
|
||||
scorePrivate: 10,
|
||||
scorePublic: 1,
|
||||
award: 0,
|
||||
contentPolicy: 'the policy',
|
||||
}
|
||||
|
||||
URL.createObjectURL = jest.fn().mockReturnValue('')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.querySelector('#previewer')!.remove()
|
||||
})
|
||||
|
||||
test('display texture name rule', () => {
|
||||
const { queryByPlaceholderText } = render(<Upload />)
|
||||
expect(queryByPlaceholderText('rule')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('content policy', () => {
|
||||
const { queryByText } = render(<Upload />)
|
||||
expect(queryByText('the policy')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('privacy notice', () => {
|
||||
const { getByLabelText, queryByText } = render(<Upload />)
|
||||
fireEvent.click(getByLabelText(t('skinlib.upload.set-as-private')))
|
||||
expect(queryByText('privacy notice')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('award notice', () => {
|
||||
Object.assign(window.blessing.extra, { award: 5 })
|
||||
|
||||
const { getByLabelText, queryByText } = render(<Upload />)
|
||||
expect(
|
||||
queryByText(t('skinlib.upload.award', { score: 5 })),
|
||||
).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByLabelText(t('skinlib.upload.set-as-private')))
|
||||
expect(
|
||||
queryByText(t('skinlib.upload.award', { score: 5 })),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('invoke selecting file', () => {
|
||||
const { getAllByText } = render(<Upload />)
|
||||
|
||||
fireEvent.click(getAllByText(t('skinlib.upload.select-file'))[1])
|
||||
})
|
||||
|
||||
describe('input file', () => {
|
||||
it('cancelled', () => {
|
||||
const { getByTitle } = render(<Upload />)
|
||||
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')))
|
||||
})
|
||||
|
||||
it('add file', () => {
|
||||
const { getByTitle, queryByDisplayValue, queryByText } = render(<Upload />)
|
||||
|
||||
const file = new File([], 't.png')
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
|
||||
expect(queryByDisplayValue('t')).toBeInTheDocument()
|
||||
expect(queryByText('t.png')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('do not overwrite existing name', () => {
|
||||
const {
|
||||
getByTitle,
|
||||
getByLabelText,
|
||||
queryByDisplayValue,
|
||||
queryByText,
|
||||
} = render(<Upload />)
|
||||
|
||||
fireEvent.input(getByLabelText(t('skinlib.upload.texture-name')), {
|
||||
target: { value: 'my texture' },
|
||||
})
|
||||
|
||||
const file = new File([], 't.png')
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
|
||||
expect(queryByDisplayValue('my texture')).toBeInTheDocument()
|
||||
expect(queryByText('t.png')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('select skin type automatically', async () => {
|
||||
isAlex.mockResolvedValue(true)
|
||||
|
||||
const { getByTitle, getByLabelText } = render(<Upload />)
|
||||
|
||||
const file = new File([], 't.png')
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
|
||||
await wait()
|
||||
expect(getByLabelText('Alex')).toBeChecked()
|
||||
})
|
||||
|
||||
it('do not overwrite "cape" type', async () => {
|
||||
const { getByTitle, getByLabelText } = render(<Upload />)
|
||||
|
||||
fireEvent.click(getByLabelText(t('general.cape')))
|
||||
await wait()
|
||||
|
||||
const file = new File([], 't.png')
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
|
||||
await wait()
|
||||
expect(getByLabelText(t('general.cape'))).toBeChecked()
|
||||
})
|
||||
})
|
||||
|
||||
describe('score cost', () => {
|
||||
it('public texture', () => {
|
||||
const { getByTitle, queryByText } = render(<Upload />)
|
||||
|
||||
const file = new File([], 't.png')
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
|
||||
expect(
|
||||
queryByText(t('skinlib.upload.cost', { score: 1 })),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('private texture', () => {
|
||||
const { getByTitle, getByLabelText, queryByText } = render(<Upload />)
|
||||
|
||||
const file = new File([], 't.png')
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
fireEvent.click(getByLabelText(t('skinlib.upload.set-as-private')))
|
||||
|
||||
expect(
|
||||
queryByText(t('skinlib.upload.cost', { score: 10 })),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('upload texture', () => {
|
||||
it('no file', () => {
|
||||
const { getByText, getByRole, queryByText } = render(<Upload />)
|
||||
fireEvent.click(getByText(t('skinlib.upload.button')))
|
||||
expect(queryByText(t('skinlib.emptyUploadFile'))).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
})
|
||||
|
||||
it('no name', () => {
|
||||
const {
|
||||
getByText,
|
||||
getByLabelText,
|
||||
getByTitle,
|
||||
getByRole,
|
||||
queryByText,
|
||||
} = render(<Upload />)
|
||||
|
||||
const file = new File([], 't.png')
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
fireEvent.input(getByLabelText(t('skinlib.upload.texture-name')), {
|
||||
target: { value: '' },
|
||||
})
|
||||
fireEvent.click(getByText(t('skinlib.upload.button')))
|
||||
|
||||
expect(queryByText(t('skinlib.emptyTextureName'))).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
})
|
||||
|
||||
it('no name', () => {
|
||||
const { getByText, getByTitle, getByRole, queryByText } = render(<Upload />)
|
||||
|
||||
const file = new File([], 't.png', { type: 'image/jpeg' })
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
fireEvent.click(getByText(t('skinlib.upload.button')))
|
||||
|
||||
expect(queryByText(t('skinlib.fileExtError'))).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
})
|
||||
|
||||
it('uploading', () => {
|
||||
fetch.post.mockResolvedValue({ code: 1, message: '' })
|
||||
|
||||
const { getByText, getByTitle, queryByText } = render(<Upload />)
|
||||
|
||||
const file = new File([], 't.png', { type: 'image/png' })
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
fireEvent.click(getByText(t('skinlib.upload.button')))
|
||||
|
||||
expect(queryByText(t('skinlib.uploading'))).toBeInTheDocument()
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/upload', expect.any(FormData))
|
||||
|
||||
const formData: FormData = fetch.post.mock.calls[0][1]
|
||||
expect(formData.get('name')).toBe('t')
|
||||
expect(formData.get('type')).toBe('steve')
|
||||
expect(formData.get('file')).toStrictEqual(file)
|
||||
expect(formData.get('public')).toBe('true')
|
||||
})
|
||||
|
||||
it('uploaded successfully', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 0, message: 'ok', tid: 1 })
|
||||
|
||||
const { getByText, getByTitle } = render(<Upload />)
|
||||
|
||||
const file = new File([], 't.png', { type: 'image/png' })
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
fireEvent.click(getByText(t('skinlib.upload.button')))
|
||||
|
||||
await wait()
|
||||
})
|
||||
|
||||
it('duplicated texture detected', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 2, message: 'dup', tid: 1 })
|
||||
|
||||
const { getByText, getByTitle, queryByText } = render(<Upload />)
|
||||
|
||||
const file = new File([], 't.png', { type: 'image/png' })
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
fireEvent.click(getByText(t('skinlib.upload.button')))
|
||||
|
||||
await wait()
|
||||
expect(queryByText('dup')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByText(t('user.viewInSkinlib')))
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByTitle, getByRole, queryByText } = render(<Upload />)
|
||||
|
||||
const file = new File([], 't.png', { type: 'image/png' })
|
||||
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
|
||||
target: { files: [file] },
|
||||
})
|
||||
fireEvent.click(getByText(t('skinlib.upload.button')))
|
||||
|
||||
await wait()
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
})
|
||||
})
|
||||
10
yarn.lock
10
yarn.lock
|
|
@ -9155,6 +9155,11 @@ sisteransi@^1.0.0:
|
|||
resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.0.tgz#77d9622ff909080f1c19e5f4a1df0c1b0a27b88c"
|
||||
integrity sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ==
|
||||
|
||||
skinview-utils@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/skinview-utils/-/skinview-utils-0.2.1.tgz#14f3f41ec812c9639dfc7b9b7745d60df1f26a84"
|
||||
integrity sha512-WhXaKwYp1FBspaKq/AXb4EG3E2gaVpA57dXWvPgM7GP2F36E8Pfdg1MZESWpTMG2TGp8xKz5JELgKD3uXSG0EA==
|
||||
|
||||
skinview3d@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/skinview3d/-/skinview3d-1.2.1.tgz#f9628940b5118112fc877872325717839f0d3976"
|
||||
|
|
@ -10381,11 +10386,6 @@ vue-template-es2015-compiler@^1.8.2, vue-template-es2015-compiler@^1.9.0:
|
|||
resolved "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue-upload-component@^2.8.20:
|
||||
version "2.8.20"
|
||||
resolved "https://registry.npmjs.org/vue-upload-component/-/vue-upload-component-2.8.20.tgz#60824d3f20f3216dca90d8c86a5c980851b04ea0"
|
||||
integrity sha512-zrnJvULu4rnZe36Ib2/AZrI/h/mmNbUJZ+acZD652PyumzbvjCOQeYHe00sGifTdYjzzS66CwhTT+ubZ2D0Aow==
|
||||
|
||||
vue@^2.6.11:
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user