From 27e6042b93ccf24e3f3169244914d2adb1368709 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Thu, 5 Mar 2020 16:16:08 +0800 Subject: [PATCH] rewrite texture upload page --- package.json | 2 +- .../src/scripts/hooks/useBlessingExtra.ts | 11 + resources/assets/src/scripts/route.tsx | 4 +- resources/assets/src/scripts/textureUtils.ts | 38 +++ resources/assets/src/views/skinlib/Upload.vue | 240 --------------- .../assets/src/views/skinlib/Upload/index.tsx | 277 ++++++++++++++++++ .../views/skinlib/Upload/styles.module.scss | 3 + .../assets/tests/__mocks__/skinview-utils.ts | 4 + .../assets/tests/ts-shims/textureUtils.ts | 1 + .../assets/tests/views/skinlib/Upload.test.ts | 187 ------------ .../tests/views/skinlib/Upload.test.tsx | 277 ++++++++++++++++++ yarn.lock | 10 +- 12 files changed, 619 insertions(+), 435 deletions(-) create mode 100644 resources/assets/src/scripts/hooks/useBlessingExtra.ts create mode 100644 resources/assets/src/scripts/textureUtils.ts delete mode 100644 resources/assets/src/views/skinlib/Upload.vue create mode 100644 resources/assets/src/views/skinlib/Upload/index.tsx create mode 100644 resources/assets/src/views/skinlib/Upload/styles.module.scss create mode 100644 resources/assets/tests/__mocks__/skinview-utils.ts create mode 100644 resources/assets/tests/ts-shims/textureUtils.ts delete mode 100644 resources/assets/tests/views/skinlib/Upload.test.ts create mode 100644 resources/assets/tests/views/skinlib/Upload.test.tsx diff --git a/package.json b/package.json index a6b2a4a9..ff5cd19a 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/resources/assets/src/scripts/hooks/useBlessingExtra.ts b/resources/assets/src/scripts/hooks/useBlessingExtra.ts new file mode 100644 index 00000000..f1426e34 --- /dev/null +++ b/resources/assets/src/scripts/hooks/useBlessingExtra.ts @@ -0,0 +1,11 @@ +import { useState, useEffect } from 'react' + +export default function useBlessingExtra(key: string): T { + const [value, setValue] = useState({} as T) + + useEffect(() => { + setValue(blessing.extra[key] as T) + }, []) + + return value +} diff --git a/resources/assets/src/scripts/route.tsx b/resources/assets/src/scripts/route.tsx index 29840ac8..ffa2af1d 100644 --- a/resources/assets/src/scripts/route.tsx +++ b/resources/assets/src/scripts/route.tsx @@ -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', }, ] diff --git a/resources/assets/src/scripts/textureUtils.ts b/resources/assets/src/scripts/textureUtils.ts new file mode 100644 index 00000000..8a898c41 --- /dev/null +++ b/resources/assets/src/scripts/textureUtils.ts @@ -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 { + 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)) + } + }) +} diff --git a/resources/assets/src/views/skinlib/Upload.vue b/resources/assets/src/views/skinlib/Upload.vue deleted file mode 100644 index e289fffb..00000000 --- a/resources/assets/src/views/skinlib/Upload.vue +++ /dev/null @@ -1,240 +0,0 @@ - - - - - diff --git a/resources/assets/src/views/skinlib/Upload/index.tsx b/resources/assets/src/views/skinlib/Upload/index.tsx new file mode 100644 index 00000000..464c05ad --- /dev/null +++ b/resources/assets/src/views/skinlib/Upload/index.tsx @@ -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('steve') + const [isPrivate, setIsPrivate] = useState(false) + const [isUploading, setIsUploading] = useState(false) + const [file, setFile] = useState(null) + const [texture, setTexture] = useState('') + const fileInput = useRef(null) + const nameRule = useBlessingExtra('rule') + const contentPolicy = useBlessingExtra('contentPolicy') + const privacyNotice = useBlessingExtra('privacyNotice') + const award = useBlessingExtra('award') + const scorePublic = useBlessingExtra('scorePublic') + const scorePrivate = useBlessingExtra('scorePrivate') + + useEffect(() => { + const mount = document.querySelector('#previewer')! + mount.appendChild(container) + + return () => { + mount.removeChild(container) + } + }, []) + + const handleNameChange = (event: React.ChangeEvent) => { + setName(event.target.value) + } + + const handleTypeChange = (event: React.ChangeEvent) => { + setType(event.target.value as TextureType) + } + + const handlePrivateChange = (event: React.ChangeEvent) => { + setIsPrivate(event.target.checked) + } + + const invokeSelectFile = () => { + fileInput.current!.click() + } + + const handleSelectFile = async ( + event: React.ChangeEvent, + ) => { + 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 ( + <> +
+
+
+ + +
+
+ +
+ + + +
+
+ +
+
+ + +
+
+ +
+
+
+ + {contentPolicy && ( +
+ )} +
+
+
+ + +
+ {file && ( +
+

+ {t('skinlib.upload.cost', { + score: (~~(file.size / 1024) || 1) * costRatio, + })} +

+
+ )} + {isPrivate && ( +
+

{privacyNotice}

+
+ )} + {!isPrivate && award > 0 && ( +
+

{t('skinlib.upload.award', { score: award })}

+
+ )} +
+
+ {ReactDOM.createPortal( + }> + + , + container, + )} + + ) +} + +export default hot(Upload) diff --git a/resources/assets/src/views/skinlib/Upload/styles.module.scss b/resources/assets/src/views/skinlib/Upload/styles.module.scss new file mode 100644 index 00000000..860c6fac --- /dev/null +++ b/resources/assets/src/views/skinlib/Upload/styles.module.scss @@ -0,0 +1,3 @@ +.label::after { + display: none; +} diff --git a/resources/assets/tests/__mocks__/skinview-utils.ts b/resources/assets/tests/__mocks__/skinview-utils.ts new file mode 100644 index 00000000..b6586813 --- /dev/null +++ b/resources/assets/tests/__mocks__/skinview-utils.ts @@ -0,0 +1,4 @@ +export function loadSkinToCanvas( + _canvas: HTMLCanvasElement, + _image: HTMLImageElement, +) {} diff --git a/resources/assets/tests/ts-shims/textureUtils.ts b/resources/assets/tests/ts-shims/textureUtils.ts new file mode 100644 index 00000000..83dd0321 --- /dev/null +++ b/resources/assets/tests/ts-shims/textureUtils.ts @@ -0,0 +1 @@ +export const isAlex = {} as jest.Mock, [string]> diff --git a/resources/assets/tests/views/skinlib/Upload.test.ts b/resources/assets/tests/views/skinlib/Upload.test.ts deleted file mode 100644 index dee09d7c..00000000 --- a/resources/assets/tests/views/skinlib/Upload.test.ts +++ /dev/null @@ -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(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(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() -}) diff --git a/resources/assets/tests/views/skinlib/Upload.test.tsx b/resources/assets/tests/views/skinlib/Upload.test.tsx new file mode 100644 index 00000000..f5f6db01 --- /dev/null +++ b/resources/assets/tests/views/skinlib/Upload.test.tsx @@ -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() + expect(queryByPlaceholderText('rule')).toBeInTheDocument() +}) + +test('content policy', () => { + const { queryByText } = render() + expect(queryByText('the policy')).toBeInTheDocument() +}) + +test('privacy notice', () => { + const { getByLabelText, queryByText } = render() + 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() + 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() + + fireEvent.click(getAllByText(t('skinlib.upload.select-file'))[1]) +}) + +describe('input file', () => { + it('cancelled', () => { + const { getByTitle } = render() + + fireEvent.change(getByTitle(t('skinlib.upload.select-file'))) + }) + + it('add file', () => { + const { getByTitle, queryByDisplayValue, queryByText } = render() + + 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() + + 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() + + 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() + + 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() + + 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() + + 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() + 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() + + 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() + + 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() + + 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() + + 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() + + 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() + + 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') + }) +}) diff --git a/yarn.lock b/yarn.lock index 30fa465d..95996a07 100644 --- a/yarn.lock +++ b/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"