From 190e54578aa7615f0f0c9b6d64556a6a29a4aea7 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 14 Aug 2018 23:27:36 +0800 Subject: [PATCH] Add index page of skin library --- app/Http/Controllers/SkinlibController.php | 9 +- resources/assets/src/components/route.js | 5 + .../assets/src/components/skinlib/List.vue | 272 ++++++++++++++++++ .../src/components/skinlib/SkinLibItem.vue | 161 +++++++++++ resources/assets/src/js/utils.js | 17 ++ .../tests/components/skinlib/List.test.js | 181 ++++++++++++ .../components/skinlib/SkinLibItem.test.js | 131 +++++++++ resources/lang/en/front-end.yml | 7 + resources/lang/zh_CN/front-end.yml | 7 + resources/views/skinlib/index.tpl | 38 +-- resources/views/skinlib/master.tpl | 50 +--- tests/SkinlibControllerTest.php | 40 +-- 12 files changed, 806 insertions(+), 112 deletions(-) create mode 100644 resources/assets/src/components/skinlib/List.vue create mode 100644 resources/assets/src/components/skinlib/SkinLibItem.vue create mode 100644 resources/assets/tests/components/skinlib/List.test.js create mode 100644 resources/assets/tests/components/skinlib/SkinLibItem.test.js diff --git a/app/Http/Controllers/SkinlibController.php b/app/Http/Controllers/SkinlibController.php index 724bc3e9..2399c17e 100644 --- a/app/Http/Controllers/SkinlibController.php +++ b/app/Http/Controllers/SkinlibController.php @@ -56,9 +56,6 @@ class SkinlibController extends Controller // Keyword to search $keyword = $request->input('keyword', ''); - // Check if user logged in - $anonymous = !Auth::check(); - if ($filter == "skin") { $query = Texture::where(function ($innerQuery) { // Nested condition, DO NOT MODIFY @@ -76,7 +73,7 @@ class SkinlibController extends Controller $query = $query->where('uploader', $uploader); } - if ($anonymous) { + if (! $currentUser) { // Show public textures only to anonymous visitors $query = $query->where('public', true); } else { @@ -95,7 +92,7 @@ class SkinlibController extends Controller ->take($itemsPerPage) ->get(); - if (! $anonymous) { + if ($currentUser) { $closet = new Closet($currentUser->uid); foreach ($textures as $item) { $item->liked = $closet->has($item->tid); @@ -104,7 +101,7 @@ class SkinlibController extends Controller return response()->json([ 'items' => $textures, - 'anonymous' => $anonymous, + 'current_uid' => $currentUser ? $currentUser->uid : 0, 'total_pages' => $totalPages ]); } diff --git a/resources/assets/src/components/route.js b/resources/assets/src/components/route.js index b133ac7c..99967821 100644 --- a/resources/assets/src/components/route.js +++ b/resources/assets/src/components/route.js @@ -54,4 +54,9 @@ export default [ component: () => import('./auth/Reset'), el: 'form' }, + { + path: 'skinlib', + component: () => import('./skinlib/List'), + el: '.content-wrapper' + }, ]; diff --git a/resources/assets/src/components/skinlib/List.vue b/resources/assets/src/components/skinlib/List.vue new file mode 100644 index 00000000..2c681044 --- /dev/null +++ b/resources/assets/src/components/skinlib/List.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/resources/assets/src/components/skinlib/SkinLibItem.vue b/resources/assets/src/components/skinlib/SkinLibItem.vue new file mode 100644 index 00000000..e0aec710 --- /dev/null +++ b/resources/assets/src/components/skinlib/SkinLibItem.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/resources/assets/src/js/utils.js b/resources/assets/src/js/utils.js index b8da1057..37b3f23f 100644 --- a/resources/assets/src/js/utils.js +++ b/resources/assets/src/js/utils.js @@ -9,3 +9,20 @@ export function debounce(func, delay) { timer = setTimeout(func, delay); }; } + +/** + * Get parameters in query string with key. + * + * @param {string} key + * @param {string} defaultValue + * @return {string} + */ +export function queryString(key, defaultValue) { + const result = location.search.match(new RegExp('[?&]'+key+'=([^&]+)', 'i')); + + if (result === null || result.length < 1){ + return defaultValue; + } else { + return result[1]; + } +} diff --git a/resources/assets/tests/components/skinlib/List.test.js b/resources/assets/tests/components/skinlib/List.test.js new file mode 100644 index 00000000..915c9686 --- /dev/null +++ b/resources/assets/tests/components/skinlib/List.test.js @@ -0,0 +1,181 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import List from '@/components/skinlib/List'; + +test('fetch data before mounting', () => { + Vue.prototype.$http.get.mockResolvedValue({ + items: [], total_pages: 0, current_uid: 0 + }); + mount(List); + expect(Vue.prototype.$http.get).toBeCalledWith( + '/skinlib/data', + { filter: 'skin', uploader: 0, sort: 'time', keyword: '', page: 1 } + ); +}); + +test('empty skin library', () => { + Vue.prototype.$http.get.mockResolvedValue({ + items: [], total_pages: 0, current_uid: 0 + }); + const wrapper = mount(List); + expect(wrapper.text()).toContain('general.noResult'); +}); + +test('toggle texture type', () => { + Vue.prototype.$http.get.mockResolvedValue({ + items: [], total_pages: 0, current_uid: 0 + }); + window.$ = jest.fn(() => ({ + iCheck() {} + })); + const wrapper = mount(List); + const breadcrumb = wrapper.find('.breadcrumb'); + + jest.runAllTimers(); + expect(breadcrumb.text()).toContain('skinlib.filter.skin'); + + wrapper.find('[value=steve]').setChecked(); + jest.runAllTimers(); + expect(breadcrumb.text()).toContain('skinlib.filter.steve'); + expect(Vue.prototype.$http.get).toBeCalledWith( + '/skinlib/data', + { filter: 'steve', uploader: 0, sort: 'time', keyword: '', page: 1 } + ); + + wrapper.find('[value=alex]').setChecked(); + jest.runAllTimers(); + expect(breadcrumb.text()).toContain('skinlib.filter.alex'); + expect(Vue.prototype.$http.get).toBeCalledWith( + '/skinlib/data', + { filter: 'alex', uploader: 0, sort: 'time', keyword: '', page: 1 } + ); + + wrapper.find('[value=cape]').setChecked(); + jest.runAllTimers(); + expect(breadcrumb.text()).toContain('general.cape'); + expect(Vue.prototype.$http.get).toBeCalledWith( + '/skinlib/data', + { filter: 'cape', uploader: 0, sort: 'time', keyword: '', page: 1 } + ); +}); + +test('check specified uploader', async () => { + Vue.prototype.$http.get.mockResolvedValue({ + items: [], total_pages: 0, current_uid: 1 + }); + const wrapper = mount(List); + await wrapper.vm.$nextTick(); + const breadcrumb = wrapper.find('.breadcrumb'); + const button = wrapper.find('.btn-default'); + + jest.runAllTimers(); + expect(breadcrumb.text()).toContain('skinlib.filter.allUsers'); + + button.trigger('click'); + expect(breadcrumb.text()).toContain('skinlib.filter.uploader'); + expect(Vue.prototype.$http.get).toBeCalledWith( + '/skinlib/data', + { filter: 'skin', uploader: 1, sort: 'time', keyword: '', page: 1 } + ); +}); + +test('sort items', () => { + Vue.prototype.$http.get.mockResolvedValue({ + items: [], total_pages: 0, current_uid: 0 + }); + const wrapper = mount(List); + const sortByLikes = wrapper.find('.dropdown-menu > li:nth-child(1) > a'); + const sortByTime = wrapper.find('.dropdown-menu > li:nth-child(2) > a'); + jest.runAllTimers(); + + sortByLikes.trigger('click'); + expect(Vue.prototype.$http.get).toBeCalledWith( + '/skinlib/data', + { filter: 'skin', uploader: 0, sort: 'likes', keyword: '', page: 1 } + ); + expect(wrapper.text()).toContain('skinlib.sort.likes'); + jest.runAllTimers(); + + sortByTime.trigger('click'); + expect(Vue.prototype.$http.get).toBeCalledWith( + '/skinlib/data', + { filter: 'skin', uploader: 0, sort: 'time', keyword: '', page: 1 } + ); + expect(wrapper.text()).toContain('skinlib.sort.time'); +}); + +test('search by keyword', () => { + Vue.prototype.$http.get.mockResolvedValue({ + items: [], total_pages: 0, current_uid: 0 + }); + const wrapper = mount(List); + const searchBox = wrapper.find('input[type=text]'); + jest.runAllTimers(); + + searchBox.setValue('a'); + wrapper.find('form').trigger('submit'); + expect(Vue.prototype.$http.get).toBeCalledWith( + '/skinlib/data', + { filter: 'skin', uploader: 0, sort: 'time', keyword: 'a', page: 1 } + ); + jest.runAllTimers(); + + searchBox.setValue('b'); + wrapper.find('.input-group-btn > button').trigger('click'); + expect(Vue.prototype.$http.get).toBeCalledWith( + '/skinlib/data', + { filter: 'skin', uploader: 0, sort: 'time', keyword: 'b', page: 1 } + ); +}); + +test('reset all filters', async () => { + window.$ = jest.fn(() => ({ + iCheck() {} + })); + Vue.prototype.$http.get.mockResolvedValue({ + items: [], total_pages: 0, current_uid: 0 + }); + const wrapper = mount(List); + jest.runAllTimers(); + wrapper.find('input[value=cape]').setChecked(); + jest.runAllTimers(); + wrapper.find('input[type=text]').setValue('abc'); + jest.runAllTimers(); + wrapper.find('.dropdown-menu > li:nth-child(1) > a').trigger('click'); + jest.runAllTimers(); + + Vue.prototype.$http.get.mockClear(); + wrapper.find('.btn-warning').trigger('click'); + expect(Vue.prototype.$http.get).toHaveBeenCalledTimes(1); +}); + +test('is anonymous', () => { + Vue.prototype.$http.get.mockResolvedValue({ + items: [], total_pages: 0, current_uid: 0 + }); + const wrapper = mount(List); + expect(wrapper.vm.anonymous).toBeTrue(); +}); + +test('on page changed', () => { + Vue.prototype.$http.get.mockResolvedValue({ + items: [], total_pages: 0, current_uid: 0 + }); + const wrapper = mount(List); + jest.runAllTimers(); + wrapper.vm.pageChanged(2); + expect(Vue.prototype.$http.get).toBeCalledWith( + '/skinlib/data', + { filter: 'skin', uploader: 0, sort: 'time', keyword: '', page: 2 } + ); +}); + +test('on like toggled', async () => { + Vue.prototype.$http.get.mockResolvedValue({ + items: [{ tid: 1, liked: false }], total_pages: 1, current_uid: 0 + }); + const wrapper = mount(List); + await wrapper.vm.$nextTick(); + wrapper.vm.onLikeToggled(0, true); + expect(wrapper.vm.items[0].liked).toBeTrue(); +}); diff --git a/resources/assets/tests/components/skinlib/SkinLibItem.test.js b/resources/assets/tests/components/skinlib/SkinLibItem.test.js new file mode 100644 index 00000000..25d9e099 --- /dev/null +++ b/resources/assets/tests/components/skinlib/SkinLibItem.test.js @@ -0,0 +1,131 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import SkinLibItem from '@/components/skinlib/SkinLibItem'; +import { flushPromises } from '../../utils'; +import { swal } from '@/js/notify'; +import toastr from 'toastr'; + +jest.mock('@/js/notify'); +jest.mock('toastr'); + +test('urls', () => { + const wrapper = mount(SkinLibItem, { + propsData: { tid: 1 } + }); + expect(wrapper.find('a').attributes()).toHaveProperty( + 'href', + '/skinlib/show/1' + ); + expect(wrapper.find('img').attributes()).toHaveProperty( + 'src', + '/preview/1.png' + ); +}); + +test('render basic information', () => { + const wrapper = mount(SkinLibItem, { + propsData: { + tid: 1, + name: 'test', + type: 'steve', + } + }); + expect(wrapper.text()).toContain('test'); + expect(wrapper.text()).toContain('skinlib.filter.steve'); +}); + +test('anonymous user', () => { + const wrapper = mount(SkinLibItem, { + propsData: { anonymous: true } + }); + const button = wrapper.find('.more'); + expect(button.attributes().title).toBe('skinlib.anonymous'); + button.trigger('click'); + expect(Vue.prototype.$http.post).not.toBeCalled(); +}); + +test('private texture', () => { + const wrapper = mount(SkinLibItem, { + propsData: { isPublic: false } + }); + expect(wrapper.text()).toContain('skinlib.private'); + + wrapper.setProps({ isPublic: true }); + expect(wrapper.text()).not.toContain('skinlib.private'); +}); + +test('liked state', () => { + const wrapper = mount(SkinLibItem, { + propsData: { liked: true, anonymous: false } + }); + const button = wrapper.find('.like'); + + expect(button.attributes()).toHaveProperty('title', 'skinlib.removeFromCloset'); + expect(button.classes()).toContain('liked'); + + wrapper.setProps({ liked: false }); + expect(button.attributes()).toHaveProperty('title', 'skinlib.addToCloset'); + expect(button.classes()).not.toContain('liked'); +}); + +test('remove from closet', async () => { + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0 }); + swal.mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({}); + jest.spyOn(toastr, 'warning'); + const wrapper = mount(SkinLibItem, { + propsData: { tid: 1, liked: true, anonymous: false } + }); + const button = wrapper.find('.like'); + + 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.emitted('like-toggled')[0]).toEqual([false]); +}); + +test('add to closet', async () => { + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0 }); + swal.mockImplementationOnce(() => ({ dismiss: 1 })) + .mockImplementation(({ inputValidator }) => { + if (inputValidator) { + inputValidator(); + inputValidator('name'); + } + return { value: 'name' }; + }); + jest.spyOn(toastr, 'warning'); + const wrapper = mount(SkinLibItem, { + propsData: { tid: 1, liked: false, anonymous: false } + }); + const button = wrapper.find('.like'); + + 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: 'name' } + ); + expect(toastr.warning).toBeCalledWith('1'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.emitted('like-toggled')[0]).toEqual([true]); +}); diff --git a/resources/lang/en/front-end.yml b/resources/lang/en/front-end.yml index 4d1ddfb3..fcfacd3e 100644 --- a/resources/lang/en/front-end.yml +++ b/resources/lang/en/front-end.yml @@ -35,12 +35,16 @@ auth: reset-button: Reset skinlib: + reset: Reset Filter + nowShowing: Now showing addToCloset: Add to closet removeFromCloset: Remove from closet setItemName: Set a name for this texture emptyItemName: Empty texture name. setNewTextureName: 'Please enter the new texture name:' emptyNewTextureName: Empty new texture name. + anyModels: Any Models + seeMyUpload: Check My Upload filter: skin: (Any Model) steve: (Steve) @@ -49,6 +53,7 @@ skinlib: uploader: 'User (UID = :uid) Uploaded' allUsers: All Users sort: + title: Sort time: Newestly Uploaded likes: Most Likes badSkinSize: The size of selected skin file is not valid @@ -276,6 +281,8 @@ general: rotation: Rotation pause: Pause reset: Reset + skinlib: Skin Library + loading: Loading user: email: Email nickname: Nick Name diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml index 178554d0..8b7c78d3 100644 --- a/resources/lang/zh_CN/front-end.yml +++ b/resources/lang/zh_CN/front-end.yml @@ -35,12 +35,16 @@ auth: reset-button: 重置 skinlib: + reset: 清除筛选 + nowShowing: 当前正显示 addToCloset: 添加至衣柜 removeFromCloset: 从衣柜中移除 setItemName: 给你的皮肤起个名字吧~ emptyItemName: 你还没有填写要收藏的材质名称啊 anonymous: 请先登录 private: 私密 + anyModels: 任意模型 + seeMyUpload: 查看我上传的 filter: skin: (任意模型) steve: (Steve) @@ -49,6 +53,7 @@ skinlib: uploader: '用户(UID = :uid)上传' allUsers: 所有用户 sort: + title: 排序 time: 最新上传 likes: 最多收藏 badSkinSize: 所选皮肤文件的尺寸不对哦 @@ -270,6 +275,8 @@ general: rotation: 旋转 pause: 暂停 reset: 重置 + skinlib: 皮肤库 + loading: 正在加载 user: email: 邮箱 nickname: 昵称 diff --git a/resources/views/skinlib/index.tpl b/resources/views/skinlib/index.tpl index d83073cd..5463afa9 100644 --- a/resources/views/skinlib/index.tpl +++ b/resources/views/skinlib/index.tpl @@ -3,41 +3,5 @@ @section('title', trans('general.skinlib')) @section('content') - -
-
- -
-

@lang('general.skinlib') - -

- -
- - -
-
- -
- - - -
- - @lang('general.loading') -
-
-
-
-
+
@endsection diff --git a/resources/views/skinlib/master.tpl b/resources/views/skinlib/master.tpl index 635173f9..2563b9d9 100644 --- a/resources/views/skinlib/master.tpl +++ b/resources/views/skinlib/master.tpl @@ -37,60 +37,12 @@
  • @lang('general.my-closet')
  • - - @unless (isset($with_out_filter)) - - - - - - @endunless - - @unless (isset($with_out_filter)) - - @endunless