diff --git a/app/Http/Controllers/SkinlibController.php b/app/Http/Controllers/SkinlibController.php index cce969d2..ff939d55 100644 --- a/app/Http/Controllers/SkinlibController.php +++ b/app/Http/Controllers/SkinlibController.php @@ -150,10 +150,7 @@ class SkinlibController extends Controller 'widgets' => [ [ ['shared.previewer'], - [ - 'skinlib.widgets.show.info', - 'skinlib.widgets.show.operations', - ], + ['skinlib.widgets.show.side'], ], ], ]; diff --git a/resources/assets/src/components/ApplyToPlayerDialog.vue b/resources/assets/src/components/ApplyToPlayerDialog.vue deleted file mode 100644 index 6273de2c..00000000 --- a/resources/assets/src/components/ApplyToPlayerDialog.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - {{ player.name }} - - - - - - - {{ $t('general.cancel') }} - - - - - - diff --git a/resources/assets/src/components/ModalInput.tsx b/resources/assets/src/components/ModalInput.tsx index a0dcf4d6..f6d01acd 100644 --- a/resources/assets/src/components/ModalInput.tsx +++ b/resources/assets/src/components/ModalInput.tsx @@ -21,6 +21,7 @@ const ModalInput: React.FC = props => ( name="modal-radios" id={`modal-radio-${choice.value}`} value={choice.value} + checked={choice.value === props.value} onChange={props.onChange} /> diff --git a/resources/assets/src/components/Portal.ts b/resources/assets/src/components/Portal.ts deleted file mode 100644 index 20c51bbe..00000000 --- a/resources/assets/src/components/Portal.ts +++ /dev/null @@ -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]) - }, -}) diff --git a/resources/assets/src/components/Previewer.vue b/resources/assets/src/components/Previewer.vue deleted file mode 100644 index 7e30cfe4..00000000 --- a/resources/assets/src/components/Previewer.vue +++ /dev/null @@ -1,178 +0,0 @@ - - - - - - - {{ indicator }} - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/assets/src/components/mixins/setAsAvatar.ts b/resources/assets/src/components/mixins/setAsAvatar.ts deleted file mode 100644 index 14b8d8a2..00000000 --- a/resources/assets/src/components/mixins/setAsAvatar.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Vue from 'vue' -import { showModal, toast } from '../../scripts/notify' - -export default Vue.extend<{ - tid: number -}, { setAsAvatar(): Promise }, {}>({ - 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('[alt="User Image"]'), - ) - .forEach(el => (el.src += `?${new Date().getTime()}`)) - } else { - toast.error(message!) - } - }, - }, -}) diff --git a/resources/assets/src/scripts/hooks/useBlessingExtra.ts b/resources/assets/src/scripts/hooks/useBlessingExtra.ts index f1426e34..b99b0f3c 100644 --- a/resources/assets/src/scripts/hooks/useBlessingExtra.ts +++ b/resources/assets/src/scripts/hooks/useBlessingExtra.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' -export default function useBlessingExtra(key: string): T { - const [value, setValue] = useState({} as T) +export default function useBlessingExtra(key: string, defaultValue?: T): T { + const [value, setValue] = useState(defaultValue!) useEffect(() => { setValue(blessing.extra[key] as T) diff --git a/resources/assets/src/scripts/hooks/useMount.ts b/resources/assets/src/scripts/hooks/useMount.ts new file mode 100644 index 00000000..5d250131 --- /dev/null +++ b/resources/assets/src/scripts/hooks/useMount.ts @@ -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 +} diff --git a/resources/assets/src/scripts/route.tsx b/resources/assets/src/scripts/route.tsx index ffa2af1d..363ed62f 100644 --- a/resources/assets/src/scripts/route.tsx +++ b/resources/assets/src/scripts/route.tsx @@ -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', diff --git a/resources/assets/src/views/skinlib/Show.vue b/resources/assets/src/views/skinlib/Show.vue deleted file mode 100644 index 0027aec3..00000000 --- a/resources/assets/src/views/skinlib/Show.vue +++ /dev/null @@ -1,442 +0,0 @@ - - - - - - - {{ $t('skinlib.addToCloset') }} - - - - - {{ $t('skinlib.apply') }} - - - {{ $t('skinlib.removeFromCloset') }} - - - {{ $t('skinlib.addToCloset') }} - - - {{ $t('user.setAsAvatar') }} - - - {{ $t('skinlib.show.download') }} - - - {{ $t('skinlib.report.title') }} - - - - - {{ likes }} - - - - - - - - - - - - - - - - - - {{ name|truncate }} - - - - - - - - - {{ $t('general.cape') }} - {{ type }} - - - {{ $t('skinlib.show.edit') }} - - - - - - Hash - - {{ hash.slice(0, 15) }}... - - - - - {{ size }} KB - - - - - {{ uploaderNickName }} - - {{ badge.text }} - - - - - - {{ uploadAt }} - - - - - - - - - - - - - - - - - - - - - - - - - - - Steve - - - - Alex - - - - {{ $t('general.cape') }} - - - - - - - - - diff --git a/resources/assets/src/views/skinlib/Show/addClosetItem.ts b/resources/assets/src/views/skinlib/Show/addClosetItem.ts new file mode 100644 index 00000000..7b9927e6 --- /dev/null +++ b/resources/assets/src/views/skinlib/Show/addClosetItem.ts @@ -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 { + 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( + '/user/closet/add', + { tid: texture.tid, name }, + ) + if (code === 0) { + toast.success(message) + } else { + toast.error(message) + } + + return code === 0 +} diff --git a/resources/assets/src/views/skinlib/Show/index.tsx b/resources/assets/src/views/skinlib/Show/index.tsx new file mode 100644 index 00000000..59b89720 --- /dev/null +++ b/resources/assets/src/views/skinlib/Show/index.tsx @@ -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({} as Texture) + const [showModalApply, setShowModalApply] = useState(false) + const [liked, setLiked] = useState(false) + const nickname = useBlessingExtra('nickname') + const currentUid = useBlessingExtra('currentUid') + const isAdmin = useBlessingExtra('admin') + const badges = useBlessingExtra('badges', []) + const canBeDownloaded = useBlessingExtra('download') + const reportScore = useBlessingExtra('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>(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( + '/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( + '/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( + '/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( + '/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( + '/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( + }> + + {currentUid === 0 ? ( + + {t('skinlib.addToCloset')} + + ) : ( + + + {liked && ( + + {t('skinlib.apply')} + + )} + {liked ? ( + + {t('skinlib.removeFromCloset')} + + ) : ( + + {t('skinlib.addToCloset')} + + )} + {texture.type !== 'cape' && ( + + {t('user.setAsAvatar')} + + )} + {canBeDownloaded && ( + + {t('skinlib.show.download')} + + )} + + {t('skinlib.report.title')} + + + + + {texture.likes} + + + )} + + , + container, + )} + + + {t('skinlib.show.detail')} + + + + + {t('skinlib.show.name')} + + {texture.name} + + {canEdit && ( + + + + )} + + + {t('skinlib.show.model')} + + {texture.type === 'cape' ? t('general.cape') : texture.type} + + {canEdit && ( + + + + )} + + + Hash + + {texture.hash} + + + + {t('skinlib.show.size')} + {texture.size} KB + + + {t('skinlib.show.uploader')} + + {nickname !== null ? ( + <> + + + {nickname} + + + + {badges.map(badge => ( + + {badge.text} + + ))} + + > + ) : ( + t('general.unexistent-user') + )} + + + + {t('skinlib.show.upload-at')} + {texture.upload_at} + + + + + {canEdit && ( + + + {t('admin.operationsTitle')} + + + {t('skinlib.show.manage-notice')} + + + + + {texture.public + ? t('skinlib.setAsPrivate') + : t('skinlib.setAsPublic')} + + + {t('skinlib.show.delete-texture')} + + + + + )} + + > + ) +} + +export default hot(Show) diff --git a/resources/assets/src/views/skinlib/Show/styles.module.scss b/resources/assets/src/views/skinlib/Show/styles.module.scss new file mode 100644 index 00000000..12ddc019 --- /dev/null +++ b/resources/assets/src/views/skinlib/Show/styles.module.scss @@ -0,0 +1,5 @@ +@use '../../../styles/utils'; + +.truncate { + @include utils.truncate-text; +} diff --git a/resources/assets/src/views/user/Closet/ClosetItem.tsx b/resources/assets/src/views/user/Closet/ClosetItem.tsx index 74d776fe..b73b9d9f 100644 --- a/resources/assets/src/views/user/Closet/ClosetItem.tsx +++ b/resources/assets/src/views/user/Closet/ClosetItem.tsx @@ -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.onClick(item) } - const setAsAvatar = async () => { - try { - await showModal({ - title: t('user.setAvatar'), - text: t('user.setAvatarNotice'), - }) - } catch { - return - } - - const { code, message } = await fetch.post( - '/user/profile/avatar', - { tid: item.tid }, - ) - if (code === 0) { - toast.success(message) - document - .querySelectorAll('[alt="User Image"]') - .forEach(el => (el.src += `?${new Date().getTime()}`)) - } else { - toast.error(message) - } - } + const handleSetAsAvatar = () => setAsAvatar(item.tid) return ( = props => { > {t('user.viewInSkinlib')} - + {t('user.setAsAvatar')} diff --git a/resources/assets/src/views/user/Closet/index.tsx b/resources/assets/src/views/user/Closet/index.tsx index a123cebe..b7b75448 100644 --- a/resources/assets/src/views/user/Closet/index.tsx +++ b/resources/assets/src/views/user/Closet/index.tsx @@ -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( - `/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) } } diff --git a/resources/assets/src/views/user/Closet/removeClosetItem.ts b/resources/assets/src/views/user/Closet/removeClosetItem.ts new file mode 100644 index 00000000..b72ea229 --- /dev/null +++ b/resources/assets/src/views/user/Closet/removeClosetItem.ts @@ -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 { + try { + await showModal({ + text: t('user.removeFromClosetNotice'), + okButtonType: 'danger', + }) + } catch { + return false + } + + const { code, message } = await fetch.post( + `/user/closet/remove/${tid}`, + ) + if (code === 0) { + toast.success(message) + } else { + toast.error(message) + } + + return code === 0 +} diff --git a/resources/assets/src/views/user/Closet/setAsAvatar.ts b/resources/assets/src/views/user/Closet/setAsAvatar.ts new file mode 100644 index 00000000..ae46a7d4 --- /dev/null +++ b/resources/assets/src/views/user/Closet/setAsAvatar.ts @@ -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( + '/user/profile/avatar', + { tid }, + ) + if (code === 0) { + toast.success(message) + document + .querySelectorAll('[alt="User Image"]') + .forEach(el => (el.src += `?${new Date().getTime()}`)) + } else { + toast.error(message) + } +} diff --git a/resources/assets/tests/components/ApplyToPlayerDialog.test.ts b/resources/assets/tests/components/ApplyToPlayerDialog.test.ts deleted file mode 100644 index 07bd74bc..00000000 --- a/resources/assets/tests/components/ApplyToPlayerDialog.test.ts +++ /dev/null @@ -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 }>(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(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 }>(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() -}) diff --git a/resources/assets/tests/components/Portal.test.ts b/resources/assets/tests/components/Portal.test.ts deleted file mode 100644 index d2386ba5..00000000 --- a/resources/assets/tests/components/Portal.test.ts +++ /dev/null @@ -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 = '' - 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 = 'before' - mount(Portal, { - propsData: { - selector: 'body', - }, - slots: { - default: 'after', - }, - }) - expect(document.querySelector('div')!.textContent).toBe('after') -}) diff --git a/resources/assets/tests/components/Previewer.test.ts b/resources/assets/tests/components/Previewer.test.ts deleted file mode 100644 index 9e7d3adf..00000000 --- a/resources/assets/tests/components/Previewer.test.ts +++ /dev/null @@ -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(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(Previewer) - wrapper.destroy() - expect(wrapper.vm.viewer.disposed).toBeTrue() -}) - -test('skin URL should be updated', () => { - const wrapper = mount(Previewer) - wrapper.setProps({ skin: 'abc' }) - expect(wrapper.vm.viewer.skinUrl).toBe('abc') -}) - -test('cape URL should be updated', () => { - const wrapper = mount(Previewer) - wrapper.setProps({ cape: 'abc' }) - expect(wrapper.vm.viewer.capeUrl).toBe('abc') -}) - -test('`footer` slot', () => { - const wrapper = mount(Previewer, { - slots: { - 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(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(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(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') -}) diff --git a/resources/assets/tests/types.d.ts b/resources/assets/tests/types.d.ts index 38195e1b..8615a8b6 100644 --- a/resources/assets/tests/types.d.ts +++ b/resources/assets/tests/types.d.ts @@ -12,7 +12,7 @@ interface Window { site_name: string version: string i18n: object - extra: object + extra: any } fetch: jest.Mock diff --git a/resources/assets/tests/views/skinlib/Show.test.ts b/resources/assets/tests/views/skinlib/Show.test.ts deleted file mode 100644 index 244327b6..00000000 --- a/resources/assets/tests/views/skinlib/Show.test.ts +++ /dev/null @@ -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(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(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(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(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(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') -}) diff --git a/resources/assets/tests/views/skinlib/Show.test.tsx b/resources/assets/tests/views/skinlib/Show.test.tsx new file mode 100644 index 00000000..cb6f05a4 --- /dev/null +++ b/resources/assets/tests/views/skinlib/Show.test.tsx @@ -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 = Object.freeze({ + tid: 1, + name: 'skin', + type: 'steve', + hash: 'abc', + size: 2, + uploader: 1, + public: true, + upload_at: new Date().toString(), + likes: 1, +}) + +const fixtureCape: Readonly = Object.freeze({ + 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() + 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() + 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() + 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() + 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() + 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() + 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() + await wait() + + fireEvent.click(getByText(t('skinlib.show.download'))) + }) + + it('not allowed', async () => { + window.blessing.extra.download = false + const { queryByText } = render() + 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() + 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() + 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( + , + ) + 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() + 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() + 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( + , + ) + 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() + 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() + 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() + 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( + , + ) + 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( + , + ) + 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() + 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() + 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() + 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() + 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( + , + ) + 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( + , + ) + 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() + 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() + 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() + 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() + 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() + 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() + 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') + }) +}) diff --git a/resources/views/skinlib/widgets/show/info.twig b/resources/views/skinlib/widgets/show/info.twig deleted file mode 100644 index 6eec664f..00000000 --- a/resources/views/skinlib/widgets/show/info.twig +++ /dev/null @@ -1 +0,0 @@ - diff --git a/resources/views/skinlib/widgets/show/operations.twig b/resources/views/skinlib/widgets/show/operations.twig deleted file mode 100644 index 400ee40a..00000000 --- a/resources/views/skinlib/widgets/show/operations.twig +++ /dev/null @@ -1 +0,0 @@ - diff --git a/resources/views/skinlib/widgets/show/side.twig b/resources/views/skinlib/widgets/show/side.twig new file mode 100644 index 00000000..8e6ec8ff --- /dev/null +++ b/resources/views/skinlib/widgets/show/side.twig @@ -0,0 +1 @@ +
{t('skinlib.show.manage-notice')}