From 2229fcf66b3aa4260356d7dbe57cc8c36264d20d Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Thu, 26 Mar 2020 18:15:13 +0800 Subject: [PATCH] rewrite "auth.forgot" page with React --- resources/assets/src/components/Alert.tsx | 28 ++++++ resources/assets/src/scripts/route.tsx | 4 +- resources/assets/src/views/auth/Forgot.tsx | 80 +++++++++++++++++ resources/assets/src/views/auth/Forgot.vue | 90 ------------------- .../assets/tests/views/auth/Forgot.test.ts | 38 -------- .../assets/tests/views/auth/Forgot.test.tsx | 51 +++++++++++ resources/views/auth/forgot.twig | 5 +- 7 files changed, 162 insertions(+), 134 deletions(-) create mode 100644 resources/assets/src/components/Alert.tsx create mode 100644 resources/assets/src/views/auth/Forgot.tsx delete mode 100644 resources/assets/src/views/auth/Forgot.vue delete mode 100644 resources/assets/tests/views/auth/Forgot.test.ts create mode 100644 resources/assets/tests/views/auth/Forgot.test.tsx diff --git a/resources/assets/src/components/Alert.tsx b/resources/assets/src/components/Alert.tsx new file mode 100644 index 00000000..722b16f2 --- /dev/null +++ b/resources/assets/src/components/Alert.tsx @@ -0,0 +1,28 @@ +import React from 'react' + +type AlertType = 'success' | 'info' | 'warning' | 'danger' + +const icons = new Map([ + ['success', 'check'], + ['info', 'info'], + ['warning', 'exclamation-triangle'], + ['danger', 'times-circle'], +]) + +interface Props { + type: AlertType +} + +const Alert: React.FC = (props) => { + const { type } = props + const icon = icons.get(type) + + return props.children ? ( +
+ + {props.children} +
+ ) : null +} + +export default Alert diff --git a/resources/assets/src/scripts/route.tsx b/resources/assets/src/scripts/route.tsx index e2c5e4cc..0628d91b 100644 --- a/resources/assets/src/scripts/route.tsx +++ b/resources/assets/src/scripts/route.tsx @@ -101,8 +101,8 @@ export default [ }, { path: 'auth/forgot', - component: () => import('../views/auth/Forgot.vue'), - el: 'form', + react: () => import('../views/auth/Forgot'), + el: 'main', }, { path: 'auth/reset/(\\d+)', diff --git a/resources/assets/src/views/auth/Forgot.tsx b/resources/assets/src/views/auth/Forgot.tsx new file mode 100644 index 00000000..f7e26e2d --- /dev/null +++ b/resources/assets/src/views/auth/Forgot.tsx @@ -0,0 +1,80 @@ +import React, { useState, useRef } from 'react' +import { hot } from 'react-hot-loader/root' +import { t } from '@/scripts/i18n' +import * as fetch from '@/scripts/net' +import Alert from '@/components/Alert' +import Captcha from '@/components/Captcha' + +const Forgot: React.FC = () => { + const [email, setEmail] = useState('') + const [isSending, setIsSending] = useState(false) + const [successMessage, setSuccessMessage] = useState('') + const [warningMessage, setWarningMessage] = useState('') + const ref = useRef(null) + + const handleEmailChange = (event: React.ChangeEvent) => { + setEmail(event.target.value) + } + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault() + setWarningMessage('') + setIsSending(true) + + const captcha = await ref.current!.execute() + const { code, message } = await fetch.post( + '/auth/forgot', + { email, captcha }, + ) + if (code === 0) { + setSuccessMessage(message) + } else { + setWarningMessage(message) + ref.current!.reset() + } + setIsSending(false) + } + + return ( +
+
+ +
+
+ +
+
+
+ + + + {successMessage} + {warningMessage} + +
+ + {t('auth.forgot.login-link')} + + +
+ + ) +} + +export default hot(Forgot) diff --git a/resources/assets/src/views/auth/Forgot.vue b/resources/assets/src/views/auth/Forgot.vue deleted file mode 100644 index 2f8739a6..00000000 --- a/resources/assets/src/views/auth/Forgot.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - diff --git a/resources/assets/tests/views/auth/Forgot.test.ts b/resources/assets/tests/views/auth/Forgot.test.ts deleted file mode 100644 index 79ee8006..00000000 --- a/resources/assets/tests/views/auth/Forgot.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Vue from 'vue' -import { mount } from '@vue/test-utils' -import Forgot from '@/views/auth/Forgot.vue' -import { flushPromises } from '../../utils' - -window.blessing.extra = {} -const Captcha = Vue.extend({ - methods: { - execute() { - return Promise.resolve('captcha') - }, - refresh() { /* */ }, - }, -}) - -test('submit forgot form', async () => { - jest.spyOn(Date, 'now') - Vue.prototype.$http.post - .mockResolvedValueOnce({ code: 1, message: 'fail' }) - .mockResolvedValueOnce({ code: 0, message: 'ok' }) - const wrapper = mount(Forgot, { stubs: { Captcha } }) - const form = wrapper.find('form') - const warning = wrapper.find('.alert-warning') - const success = wrapper.find('.alert-success') - - wrapper.find('[type="email"]').setValue('a@b.c') - form.trigger('submit') - await flushPromises() - expect(Vue.prototype.$http.post).toBeCalledWith( - '/auth/forgot', - { email: 'a@b.c', captcha: 'captcha' }, - ) - expect(warning.text()).toBe('fail') - - form.trigger('submit') - await flushPromises() - expect(success.text()).toBe('ok') -}) diff --git a/resources/assets/tests/views/auth/Forgot.test.tsx b/resources/assets/tests/views/auth/Forgot.test.tsx new file mode 100644 index 00000000..d50832a6 --- /dev/null +++ b/resources/assets/tests/views/auth/Forgot.test.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import { render, wait, fireEvent } from '@testing-library/react' +import { t } from '@/scripts/i18n' +import * as fetch from '@/scripts/net' +import Forgot from '@/views/auth/Forgot' + +jest.mock('@/scripts/net') + +describe('submit', () => { + it('succeeded', async () => { + fetch.post.mockResolvedValue({ code: 0, message: 'ok' }) + + const { getByPlaceholderText, getByText, queryByText } = render() + + fireEvent.input(getByPlaceholderText(t('auth.email')), { + target: { value: 'a@b.c' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.captcha')), { + target: { value: 'abc' }, + }) + fireEvent.click(getByText(t('auth.forgot.button'))) + await wait() + + expect(fetch.post).toBeCalledWith('/auth/forgot', { + email: 'a@b.c', + captcha: 'abc', + }) + expect(queryByText('ok')).toBeInTheDocument() + }) + + it('failed', async () => { + fetch.post.mockResolvedValue({ code: 1, message: 'failed' }) + + const { getByPlaceholderText, getByText, queryByText } = render() + + fireEvent.input(getByPlaceholderText(t('auth.email')), { + target: { value: 'a@b.c' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.captcha')), { + target: { value: 'abc' }, + }) + fireEvent.click(getByText(t('auth.forgot.button'))) + await wait() + + expect(fetch.post).toBeCalledWith('/auth/forgot', { + email: 'a@b.c', + captcha: 'abc', + }) + expect(queryByText('failed')).toBeInTheDocument() + }) +}) diff --git a/resources/views/auth/forgot.twig b/resources/views/auth/forgot.twig index 6bddb4db..7c5a1dc6 100644 --- a/resources/views/auth/forgot.twig +++ b/resources/views/auth/forgot.twig @@ -9,13 +9,10 @@ {{ session_pull('msg') }} {% endif %} -
+
{% endblock %} {% block before_foot %} - {% if enable_recaptcha %} - - {% endif %}