From deda5cf11c1692dfa45f7adb619b52e13739ae17 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Thu, 16 Aug 2018 16:34:27 +0800 Subject: [PATCH] Add skin library "show" page --- resources/assets/src/components/route.js | 5 + .../assets/src/components/skinlib/Show.vue | 308 +++++++++++++++++ .../tests/components/skinlib/Show.test.js | 317 ++++++++++++++++++ resources/lang/en/front-end.yml | 14 + resources/lang/zh_CN/front-end.yml | 14 + resources/views/skinlib/show.tpl | 133 +------- 6 files changed, 671 insertions(+), 120 deletions(-) create mode 100644 resources/assets/src/components/skinlib/Show.vue create mode 100644 resources/assets/tests/components/skinlib/Show.test.js diff --git a/resources/assets/src/components/route.js b/resources/assets/src/components/route.js index 8054a370..869fabf9 100644 --- a/resources/assets/src/components/route.js +++ b/resources/assets/src/components/route.js @@ -59,6 +59,11 @@ export default [ component: () => import('./skinlib/List'), el: '.content-wrapper' }, + { + path: 'skinlib/show/(\\d+)', + component: () => import('./skinlib/Show'), + el: '.content > .row:nth-child(1)' + }, { path: 'skinlib/upload', component: () => import('./skinlib/Upload'), diff --git a/resources/assets/src/components/skinlib/Show.vue b/resources/assets/src/components/skinlib/Show.vue new file mode 100644 index 00000000..d64fa028 --- /dev/null +++ b/resources/assets/src/components/skinlib/Show.vue @@ -0,0 +1,308 @@ + + + + + diff --git a/resources/assets/tests/components/skinlib/Show.test.js b/resources/assets/tests/components/skinlib/Show.test.js new file mode 100644 index 00000000..6b327c49 --- /dev/null +++ b/resources/assets/tests/components/skinlib/Show.test.js @@ -0,0 +1,317 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import Show from '@/components/skinlib/Show'; +import { flushPromises } from '../../utils'; +import { swal } from '@/js/notify'; +import toastr from 'toastr'; + +jest.mock('@/js/notify'); + +window.__bs_data__ = { + download: true, + currentUid: 0, + admin: false, + nickname: 'author', + inCloset: false, +}; + +/** @type {import('Vue').ComponentOptions} */ +const previewer = { + 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({}); + const wrapper = mount(Show, { + mocks: { + $route: ['/skinlib/show/1', '1'] + }, + stubs: { previewer } + }); + expect(wrapper.find('.btn-primary').attributes()).toHaveProperty('disabled', 'disabled'); +}); + +test('button for adding to closet should be disabled if auth', () => { + Vue.prototype.$http.get.mockResolvedValue({}); + Object.assign(window.__bs_data__, { inCloset: true, currentUid: 1 }); + const wrapper = mount(Show, { + mocks: { + $route: ['/skinlib/show/1', '1'] + }, + stubs: { previewer } + }); + expect(wrapper.find('.btn-primary').text()).toBe('skinlib.removeFromCloset'); +}); + +test('likes count indicator', async () => { + Vue.prototype.$http.get.mockResolvedValue({ likes: 2 }); + const wrapper = mount(Show, { + mocks: { + $route: ['/skinlib/show/1', '1'] + }, + stubs: { previewer } + }); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.likes').attributes().style).toContain('color: rgb(224, 53, 59)'); + expect(wrapper.find('.likes').text()).toContain('2'); +}); + +test('render basic information', async () => { + Vue.prototype.$http.get.mockResolvedValue({ + name: 'my-texture', + type: 'alex', + hash: '123', + size: 2, + upload_at: '2018' + }); + const wrapper = mount(Show, { + mocks: { + $route: ['/skinlib/show/1', '1'] + } + }); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.box-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.__bs_data__, { admin: true }); + Vue.prototype.$http.get.mockResolvedValue({ uploader: 1, name: 'name' }); + + let wrapper = mount(Show, { + mocks: { + $route: ['/skinlib/show/1', '1'] + } + }); + await wrapper.vm.$nextTick(); + expect(wrapper.contains('small')).toBeTrue(); + + Object.assign(window.__bs_data__, { currentUid: 2, admin: false }); + wrapper = mount(Show, { + mocks: { + $route: ['/skinlib/show/1', '1'] + } + }); + await wrapper.vm.$nextTick(); + expect(wrapper.contains('small')).toBeFalse(); +}); + +test('render nickname of uploader', () => { + Object.assign(window.__bs_data__, { nickname: null }); + Vue.prototype.$http.get.mockResolvedValue({}); + 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 not auth', () => { + Object.assign(window.__bs_data__, { currentUid: 0 }); + Vue.prototype.$http.get.mockResolvedValue({}); + const wrapper = mount(Show, { + mocks: { + $route: ['/skinlib/show/1', '1'] + } + }); + expect(wrapper.contains('.box-warning')).toBeFalse(); +}); + +test('link to downloading texture', async () => { + Object.assign(window.__bs_data__, { download: false }); + Vue.prototype.$http.get.mockResolvedValue({ hash: '123' }); + const wrapper = mount(Show, { + mocks: { + $route: ['/skinlib/show/1', '1'] + } + }); + await wrapper.vm.$nextTick(); + expect(wrapper.contains('a[title="123"]')).toBeFalse(); + expect(wrapper.contains('span[title="123"]')).toBeTrue(); +}); + +test('add to closet', async () => { + Object.assign(window.__bs_data__, { currentUid: 1, inCloset: false }); + Vue.prototype.$http.get.mockResolvedValue({ name: 'wow', likes: 2 }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0, msg: '' }); + jest.spyOn(toastr, 'warning'); + swal.mockImplementationOnce(() => ({ dismiss: 1 })) + .mockImplementation(({ inputValidator }) => { + if (inputValidator) { + inputValidator(); + inputValidator('wow'); + } + return { value: 'wow' }; + }); + const wrapper = mount(Show, { + mocks: { + $route: ['/skinlib/show/1', '1'] + }, + stubs: { previewer } + }); + const button = wrapper.find('.btn-primary'); + + button.trigger('click'); + expect(Vue.prototype.$http.post).not.toBeCalled(); + + button.trigger('click'); + await flushPromises(); + expect(Vue.prototype.$http.post).toBeCalledWith( + '/user/closet/add', + { tid: 1, name: 'wow' } + ); + expect(toastr.warning).toBeCalledWith('1'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.vm.likes).toBe(3); + expect(wrapper.vm.liked).toBeTrue(); +}); + +test('remove from closet', async () => { + Object.assign(window.__bs_data__, { currentUid: 1, inCloset: true }); + Vue.prototype.$http.get.mockResolvedValue({ likes: 2 }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0, msg: '' }); + jest.spyOn(toastr, 'warning'); + swal.mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({}); + const wrapper = mount(Show, { + mocks: { + $route: ['/skinlib/show/1', '1'] + }, + stubs: { previewer } + }); + const button = wrapper.find('.btn-primary'); + + button.trigger('click'); + expect(Vue.prototype.$http.post).not.toBeCalled(); + + button.trigger('click'); + await flushPromises(); + expect(Vue.prototype.$http.post).toBeCalledWith( + '/user/closet/remove', + { tid: 1 } + ); + expect(toastr.warning).toBeCalledWith('1'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.vm.likes).toBe(1); + expect(wrapper.vm.liked).toBeFalse(); +}); + +test('change texture name', async () => { + Object.assign(window.__bs_data__, { admin: true }); + Vue.prototype.$http.get.mockResolvedValue({ name: 'old-name' }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0, msg: '0' }); + jest.spyOn(toastr, 'warning'); + swal.mockImplementationOnce(() => ({ dismiss: 1 })) + .mockImplementation(({ inputValidator }) => { + inputValidator(); + inputValidator('new-name'); + return { 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(toastr.warning).toBeCalledWith('1'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.vm.name).toBe('new-name'); +}); + +test('toggle privacy', async () => { + Vue.prototype.$http.get.mockResolvedValue({ public: true }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0, msg: '0' }); + jest.spyOn(toastr, 'warning'); + swal.mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({}); + 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(toastr.warning).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({}); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0, msg: '0' }); + swal.mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({}); + 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(swal).toBeCalledWith({ type: 'warning', text: '1' }); + + button.trigger('click'); + await flushPromises(); + expect(swal).toBeCalledWith({ type: 'success', text: '0' }); +}); diff --git a/resources/lang/en/front-end.yml b/resources/lang/en/front-end.yml index 28e2ebcd..e5a8bbef 100644 --- a/resources/lang/en/front-end.yml +++ b/resources/lang/en/front-end.yml @@ -68,6 +68,7 @@ skinlib: setAsPrivate: Set as Private setAsPublic: Set as Public setPublicNotice: Sure to set this as public texture? + setPrivateNotice: Sure to set this as private texture? deleteNotice: Are you sure to delete this texture? upload: texture-name: Texture Name @@ -79,6 +80,19 @@ skinlib: dropZone: Drop a file here remove: Remove cost: (It cost you about :score score) + show: + anonymous: You must login to use closets + likes: People who like this + detail: Details + name: Texture Name + edit-name: Edit Name + model: Applicable Model + download-raw: Click to download raw texture + size: File Size + uploader: Uploader + upload-at: Upload At + delete-texture: Delete Texture + notice: The texture which was deleted or setted to private will be removed from the closet of everyone who had favorited it. user: signRemainingTime: 'Available after :time :unit' diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml index cd41ed88..d7d6ee22 100644 --- a/resources/lang/zh_CN/front-end.yml +++ b/resources/lang/zh_CN/front-end.yml @@ -70,6 +70,7 @@ skinlib: setAsPrivate: 设为隐私 setAsPublic: 设为公开 setPublicNotice: 要将此材质设置为公开吗? + setPrivateNotice: 要将此材质设置为私有吗? deleteNotice: 真的要删除此材质吗? upload: texture-name: 材质名称 @@ -81,6 +82,19 @@ skinlib: dropZone: 拖拽文件到这里 remove: 移除 cost: (这会消耗您约 :score 积分) + show: + anonymous: 登录后才能使用衣柜哦 + likes: 收藏人数 + detail: 详细信息 + name: 名称 + edit-name: 修改名称 + model: 适用模型 + download-raw: 右键另存为即可下载原始皮肤文件 + size: 文件大小 + uploader: 上传者 + upload-at: 上传日期 + delete-texture: 删除材质 + manage-notice: 材质设为隐私或被删除后将会从每一个收藏者的衣柜中移除。 user: signRemainingTime: ':time :unit 后可签到' diff --git a/resources/views/skinlib/show.tpl b/resources/views/skinlib/show.tpl index d4675b4b..a1d80ddf 100644 --- a/resources/views/skinlib/show.tpl +++ b/resources/views/skinlib/show.tpl @@ -15,108 +15,7 @@
-
-
-
- @include('common.texture-preview') - - -
-
-
-
-
-

@lang('skinlib.show.detail')

-
-
- - - - - - - - - - - - - - - - - - - - - @if ($uploader = app('users')->get($texture->uploader)) - - @else - - @endif - - - - - - -
@lang('skinlib.show.name'){{ $texture->name }} - @if (!is_null($user) && ($texture->uploader == $user->uid || $user->isAdmin())) - - @lang('skinlib.show.edit-name') - - @endif -
@lang('skinlib.show.model') - @if ($texture->type == 'cape') - @lang('general.cape') - @else - {{ $texture->type }} - @endif -
Hash - @if (option('allow_downloading_texture')) - - @endif - - @if (option('allow_downloading_texture')) - {{ substr($texture->hash, 0, 15) }}... - @else - {{ substr($texture->hash, 0, 15) }}... - @endif -
@lang('skinlib.show.size'){{ $texture->size }} KB
@lang('skinlib.show.uploader'){{ $uploader->getNickName() }}@lang('general.unexistent-user')
@lang('skinlib.show.upload-at'){{ $texture->upload_at }}
-
-
- - @if (!is_null($user)) - @if ($texture->uploader == $user->uid) - @include('common.manage-panel', [ - 'title' => trans('skinlib.show.delete-texture')." / ".trans('skinlib.privacy.change-privacy'), - 'message' => trans('skinlib.show.notice') - ]) - - @elseif ($user->isAdmin()) - @include('common.manage-panel', [ - 'title' => trans('skinlib.show.manage-panel'), - 'message' => trans('skinlib.show.notice-admin') - ]) - @endif - @endif -
-
+
@@ -141,25 +40,19 @@
-@endsection - -@section('script') + @endsection