From 3dfa5c6e4e4612b58e416855782bbeec1c273e26 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Thu, 2 Aug 2018 17:29:43 +0800 Subject: [PATCH] Add profile page --- .eslintrc.yml | 1 + resources/assets/src/components/route.js | 7 +- .../assets/src/components/user/Profile.vue | 299 ++++++++++++++++++ .../tests/components/user/Profile.test.js | 179 +++++++++++ resources/assets/tests/setup.js | 3 +- resources/lang/en/front-end.yml | 34 ++ resources/lang/zh_CN/front-end.yml | 35 ++ resources/views/user/master.tpl | 2 + resources/views/user/profile.tpl | 123 +------ 9 files changed, 570 insertions(+), 113 deletions(-) create mode 100644 resources/assets/src/components/user/Profile.vue create mode 100644 resources/assets/tests/components/user/Profile.test.js diff --git a/.eslintrc.yml b/.eslintrc.yml index 4504d921..3dbda289 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -41,3 +41,4 @@ rules: globals: blessing: false + __bs_data__: false diff --git a/resources/assets/src/components/route.js b/resources/assets/src/components/route.js index f5ce89e7..7c5e5326 100644 --- a/resources/assets/src/components/route.js +++ b/resources/assets/src/components/route.js @@ -3,5 +3,10 @@ export default [ path: 'user/closet', component: () => import('./user/closet'), el: '.content' - } + }, + { + path: 'user/profile', + component: () => import('./user/profile'), + el: '.content' + }, ]; diff --git a/resources/assets/src/components/user/Profile.vue b/resources/assets/src/components/user/Profile.vue new file mode 100644 index 00000000..2d955050 --- /dev/null +++ b/resources/assets/src/components/user/Profile.vue @@ -0,0 +1,299 @@ + + + diff --git a/resources/assets/tests/components/user/Profile.test.js b/resources/assets/tests/components/user/Profile.test.js new file mode 100644 index 00000000..0b10153d --- /dev/null +++ b/resources/assets/tests/components/user/Profile.test.js @@ -0,0 +1,179 @@ +import { mount } from '@vue/test-utils'; +import Profile from '@/components/user/Profile'; +import axios from 'axios'; +import toastr from 'toastr'; +import { swal } from '@/js/notify'; + +jest.mock('axios'); +jest.mock('@/js/notify'); + +test('computed values', () => { + window.__bs_data__ = { admin: true }; + const wrapper = mount(Profile); + expect(wrapper.vm.siteName).toBe('Blessing Skin'); + expect(wrapper.vm.isAdmin).toBeTrue(); + window.__bs_data__ = { admin: false }; + expect(mount(Profile).vm.isAdmin).toBeFalse(); +}); + +test('convert linebreak', () => { + const wrapper = mount(Profile); + expect(wrapper.vm.nl2br('a\nb\nc')).toBe('a
b
c'); +}); + +test('change password', async () => { + jest.spyOn(toastr, 'info'); + axios.post + .mockResolvedValueOnce({ data: { errno: 1, msg: 'w' } }) + .mockResolvedValueOnce({ data: { errno: 0, msg: 'o' } }); + swal.mockResolvedValue(); + const wrapper = mount(Profile); + const button = wrapper.find('[data-test=changePassword]'); + + button.trigger('click'); + expect(toastr.info).toBeCalledWith('user.emptyPassword'); + expect(axios.post).not.toBeCalled(); + + wrapper.setData({ oldPassword: '1' }); + button.trigger('click'); + expect(toastr.info).toBeCalledWith('user.emptyNewPassword'); + expect(axios.post).not.toBeCalled(); + + wrapper.setData({ newPassword: '1' }); + button.trigger('click'); + expect(toastr.info).toBeCalledWith('auth.emptyConfirmPwd'); + expect(axios.post).not.toBeCalled(); + + wrapper.setData({ confirmPassword: '2' }); + button.trigger('click'); + expect(toastr.info).toBeCalledWith('auth.invalidConfirmPwd'); + expect(axios.post).not.toBeCalled(); + + wrapper.setData({ confirmPassword: '1' }); + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(axios.post).toBeCalledWith( + '/user/profile?action=password', + { current_password: '1', new_password: '1' } + ); + expect(swal).toBeCalledWith({ type: 'warning', text: 'w' }); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(swal).toBeCalledWith({ type: 'success', text: 'o' }); +}); + +test('change nickname', async () => { + axios.post + .mockResolvedValueOnce({ data: { errno: 1, msg: 'w' } }) + .mockResolvedValue({ data: { errno: 0, msg: 'o' } }); + swal.mockResolvedValueOnce({}) + .mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({}); + window.$ = jest.fn(() => ({ + each(fn) { + fn(); + }, + html() {} + })); + const wrapper = mount(Profile); + const button = wrapper.find('[data-test=changeNickName]'); + + button.trigger('click'); + expect(axios.post).not.toBeCalled(); + expect(swal).toBeCalledWith({ type: 'error', html: 'user.emptyNewNickName' }); + + wrapper.setData({ nickname: 'nickname' }); + button.trigger('click'); + expect(axios.post).not.toBeCalled(); + expect(swal).toBeCalledWith({ + text: 'user.changeNickName', + type: 'question', + showCancelButton: true + }); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(axios.post).toBeCalledWith( + '/user/profile?action=nickname', + { new_nickname: 'nickname' } + ); + await wrapper.vm.$nextTick(); + expect(swal).toBeCalledWith({ type: 'warning', html: 'w' }); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + expect(swal).toBeCalledWith({ type: 'success', html: 'o' }); +}); + +test('change email', async () => { + axios.post + .mockResolvedValueOnce({ data: { errno: 1, msg: 'w' } }) + .mockResolvedValue({ data: { errno: 0, msg: 'o' } }); + swal.mockResolvedValueOnce({}) + .mockResolvedValueOnce({}) + .mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({}); + const wrapper = mount(Profile); + const button = wrapper.find('[data-test=changeEmail]'); + + button.trigger('click'); + expect(swal).toBeCalledWith({ type: 'error', html: 'user.emptyNewEmail' }); + expect(axios.post).not.toBeCalled(); + + wrapper.setData({ email: 'e' }); + button.trigger('click'); + expect(swal).toBeCalledWith({ type: 'warning', html: 'auth.invalidEmail' }); + expect(axios.post).not.toBeCalled(); + + wrapper.setData({ email: 'a@b.c', currentPassword: 'abc' }); + button.trigger('click'); + expect(swal).toBeCalledWith({ + text: 'user.changeEmail', + type: 'question', + showCancelButton: true + }); + expect(axios.post).not.toBeCalled(); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(axios.post).toBeCalledWith( + '/user/profile?action=email', + { new_email: 'a@b.c', password: 'abc' } + ); + await wrapper.vm.$nextTick(); + expect(swal).toBeCalledWith({ type: 'warning', text: 'w' }); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); // There are two promises, so call it twice. + expect(swal).toBeCalledWith({ type: 'success', text: 'o' }); +}); + +test('delete account', async () => { + window.__bs_data__ = { admin: true }; + swal.mockResolvedValue(); + axios.post + .mockResolvedValueOnce({ data: { errno: 1, msg: 'w' } }) + .mockResolvedValue({ data: { errno: 0, msg: 'o' } }); + const wrapper = mount(Profile); + const button = wrapper.find('[data-test=deleteAccount]'); + + button.trigger('click'); + expect(swal).toBeCalledWith({ type: 'warning', html: 'user.emptyDeletePassword' }); + expect(axios.post).not.toBeCalled(); + + wrapper.setData({ deleteConfirm: 'abc' }); + button.trigger('click'); + expect(axios.post).toBeCalledWith( + '/user/profile?action=delete', + { password: 'abc' } + ); + await wrapper.vm.$nextTick(); + expect(swal).toBeCalledWith({ type: 'warning', html: 'w' }); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(swal).toBeCalledWith({ type: 'success', html: 'o' }); +}); diff --git a/resources/assets/tests/setup.js b/resources/assets/tests/setup.js index 96d5ab9a..835f9da7 100644 --- a/resources/assets/tests/setup.js +++ b/resources/assets/tests/setup.js @@ -2,7 +2,8 @@ import 'jest-extended'; import Vue from 'vue'; window.blessing = { - base_url: '' + base_url: '', + site_name: 'Blessing Skin' }; console.log = console.warn = console.error = () => {}; diff --git a/resources/lang/en/front-end.yml b/resources/lang/en/front-end.yml index 34f9d3c6..b7e981fb 100644 --- a/resources/lang/en/front-end.yml +++ b/resources/lang/en/front-end.yml @@ -93,6 +93,39 @@ user: title: Which player should be applied to? empty: It seems that you own no player... add: Add new player + profile: + avatar: + title: Change Avatar? + notice: Click the gear icon「」of any skin in your skinlib, then click 「Set as avatar」. We will cut the head segment of that skin for you. If there is no icon like this, please unable the extensions like ADBlock. + + password: + title: Change Password + old: Old Password + new: New Password + confirm: Repeat Password + button: Change Password + + nickname: + title: Change Nickname + rule: Whatever you like expect special characters + + email: + title: Change Email + new: New Email + password: Current Password + button: Change Email + + delete: + title: Delete Account + notice: Sure to delete your account on :site? + admin: Admin account can not be deleted. + button: Delete My Account + modal-title: You need to enter your password to continue + modal-notice: | + You're about to delete your account. + This is permanent! No backups, no restores, no magic undo button. + We warned you, ok? + password: Current Password admin: operationsTitle: Operations @@ -162,6 +195,7 @@ general: confirm: OK cancel: Cancel submit: Submit + close: Close more: More pagination: 'Page :page, total :total' searchResult: '(Search result of keyword ":keyword")' diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml index b086e804..b1e5891a 100644 --- a/resources/lang/zh_CN/front-end.yml +++ b/resources/lang/zh_CN/front-end.yml @@ -93,6 +93,40 @@ user: title: 要给哪个角色使用呢? empty: 你好像还没有添加任何角色哦 add: 添加角色 + profile: + avatar: + title: 更改头像? + notice: 请在衣柜中任意皮肤的右下角「」处选择「设为头像」,将会自动截取该皮肤的头部作为头像哦~ 如果看不到这个图标,请关闭 ADBlock,ABP 之类的广告过滤扩展。 + + password: + title: 更改密码 + old: 旧的密码 + new: 新密码 + confirm: 确认密码 + button: 修改密码 + + nickname: + title: 更改昵称 + rule: 可使用除一些特殊符号外的任意字符 + + email: + title: 更改邮箱 + new: 新邮箱 + password: 当前密码 + button: 修改邮箱 + + delete: + title: 删除账号 + notice: 确定要删除你在 :site 上的账号吗? + admin: 管理员账号不能被删除哟 + button: 删除我的账户 + modal-title: 这是危险操作,输入密码以继续 + modal-notice: | + 此操作不可恢复! + 你所上传至皮肤库的材质仍会被保留,但你的角色将被永久删除。 + 我们不提供任何备份,或者神奇的撤销按钮。 + 我们警告过你了,确定要这样做吗? + password: 当前密码 admin: operationsTitle: 更多操作 @@ -158,6 +192,7 @@ general: confirm: 确定 cancel: 取消 submit: 提交 + close: 关闭 more: 更多 pagination: '第 :page 页,共 :total 页' searchResult: '(关键词 “:keyword” 的搜索结果)' diff --git a/resources/views/user/master.tpl b/resources/views/user/master.tpl index 52c58d12..e9fbc447 100644 --- a/resources/views/user/master.tpl +++ b/resources/views/user/master.tpl @@ -91,6 +91,8 @@ + @yield('pre-script') + @include('common.dependencies.script', ['module' => 'user']) diff --git a/resources/views/user/profile.tpl b/resources/views/user/profile.tpl index 6fe2aef9..c9679070 100644 --- a/resources/views/user/profile.tpl +++ b/resources/views/user/profile.tpl @@ -14,117 +14,18 @@ -
-
-
-
-
-

@lang('user.profile.avatar.title')

-
-
- {!! trans('user.profile.avatar.notice') !!} -
-
- -
-
-

@lang('user.profile.password.title')

-
-
-
- - -
- -
- - -
- -
- - -
-
- -
-
-
-
-
-

@lang('user.profile.nickname.title')

-
-
-
- - -
-
- -
- -
-
-

@lang('user.profile.email.title')

-
-
-
- - -
- -
- -
- -
-
-

@lang('user.profile.delete.title')

-
-
- @if (!$user->isAdmin()) -

@lang('user.profile.delete.notice', ['site' => option_localized('site_name')])

- - @else -

@lang('user.profile.delete.admin')

- - @endif -
-
-
-
- -
+
- - +@endsection + +@section('pre-script') + @endsection