rewrite texture upload page

This commit is contained in:
Pig Fang 2020-03-05 16:16:08 +08:00
parent d4c3264324
commit 27e6042b93
12 changed files with 619 additions and 435 deletions

View File

@ -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"

View 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
}

View File

@ -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',
},
]

View 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))
}
})
}

View File

@ -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>

View 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)

View File

@ -0,0 +1,3 @@
.label::after {
display: none;
}

View File

@ -0,0 +1,4 @@
export function loadSkinToCanvas(
_canvas: HTMLCanvasElement,
_image: HTMLImageElement,
) {}

View File

@ -0,0 +1 @@
export const isAlex = {} as jest.Mock<Promise<boolean>, [string]>

View File

@ -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()
})

View 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')
})
})

View File

@ -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"