From af506467a210d0c24ef865f4df00297f896ad49a Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Tue, 29 Oct 2024 11:38:18 +0100 Subject: [PATCH 1/5] fix(editor): Auto focus email field on sign-in view --- packages/editor-ui/src/views/SigninView.test.ts | 15 +++++++++++++++ packages/editor-ui/src/views/SigninView.vue | 1 + 2 files changed, 16 insertions(+) create mode 100644 packages/editor-ui/src/views/SigninView.test.ts diff --git a/packages/editor-ui/src/views/SigninView.test.ts b/packages/editor-ui/src/views/SigninView.test.ts new file mode 100644 index 0000000000000..a41f17b9fef00 --- /dev/null +++ b/packages/editor-ui/src/views/SigninView.test.ts @@ -0,0 +1,15 @@ +import { createComponentRenderer } from '@/__tests__/render'; +import { createTestingPinia } from '@pinia/testing'; +import SigninView from '@/views/SigninView.vue'; + +const renderComponent = createComponentRenderer(SigninView); + +describe('SigninView', () => { + beforeEach(() => { + createTestingPinia(); + }); + + it('should not throw error when opened', () => { + expect(() => renderComponent()).not.toThrow(); + }); +}); diff --git a/packages/editor-ui/src/views/SigninView.vue b/packages/editor-ui/src/views/SigninView.vue index fa258c5d7adac..f0c203d469416 100644 --- a/packages/editor-ui/src/views/SigninView.vue +++ b/packages/editor-ui/src/views/SigninView.vue @@ -60,6 +60,7 @@ const formConfig: IFormBoxConfig = reactive({ validateOnBlur: false, autocomplete: 'email', capitalize: true, + focusInitially: true, }, }, { From 6569dca32759ddbfcd71bed3e27f9ad5827a8178 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Tue, 29 Oct 2024 16:54:49 +0100 Subject: [PATCH 2/5] test(editor): Test Signin --- .../editor-ui/src/views/SigninView.test.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/packages/editor-ui/src/views/SigninView.test.ts b/packages/editor-ui/src/views/SigninView.test.ts index a41f17b9fef00..d1bbb9b020d04 100644 --- a/packages/editor-ui/src/views/SigninView.test.ts +++ b/packages/editor-ui/src/views/SigninView.test.ts @@ -1,15 +1,100 @@ import { createComponentRenderer } from '@/__tests__/render'; +import { mockedStore } from '@/__tests__/utils'; import { createTestingPinia } from '@pinia/testing'; +import userEvent from '@testing-library/user-event'; +import { useRouter } from 'vue-router'; import SigninView from '@/views/SigninView.vue'; +import { useUsersStore } from '@/stores/users.store'; +import { useSettingsStore } from '@/stores/settings.store'; +import { useTelemetry } from '@/composables/useTelemetry'; + +vi.mock('vue-router', () => { + const push = vi.fn(); + return { + useRouter: () => ({ + push, + }), + useRoute: () => ({ + query: { + redirect: '/home/workflows', + }, + }), + RouterLink: { + template: '', + }, + }; +}); + +vi.mock('@/composables/useTelemetry', () => { + const track = vi.fn(); + return { + useTelemetry: () => ({ + track, + }), + }; +}); const renderComponent = createComponentRenderer(SigninView); +let usersStore: ReturnType>; +let settingsStore: ReturnType>; + +let router: ReturnType; +let telemetry: ReturnType; + describe('SigninView', () => { beforeEach(() => { createTestingPinia(); + usersStore = mockedStore(useUsersStore); + settingsStore = mockedStore(useSettingsStore); + + router = useRouter(); + telemetry = useTelemetry(); }); it('should not throw error when opened', () => { expect(() => renderComponent()).not.toThrow(); }); + + it('should show and submit email/password form (happy path)', async () => { + settingsStore.isCloudDeployment = false; + usersStore.loginWithCreds.mockResolvedValueOnce(); + + const { getByRole, queryByTestId, container } = renderComponent(); + const emailInput = container.querySelector('input[type="email"]'); + const passwordInput = container.querySelector('input[type="password"]'); + const submitButton = getByRole('button', { name: 'Sign in' }); + + if (!emailInput || !passwordInput) { + throw new Error('Inputs not found'); + } + + expect(queryByTestId('mfa-login-form')).not.toBeInTheDocument(); + + expect(emailInput).toBeVisible(); + expect(passwordInput).toBeVisible(); + + // TODO: Remove manual tabbing when the following issue is fixed (it should fail the test anyway) + // https://github.com/testing-library/vue-testing-library/issues/317 + await userEvent.tab(); + expect(document.activeElement).toBe(emailInput); + + await userEvent.type(emailInput, 'test@n8n.io'); + await userEvent.type(passwordInput, 'password'); + + await userEvent.click(submitButton); + + expect(usersStore.loginWithCreds).toHaveBeenCalledWith({ + email: 'test@n8n.io', + password: 'password', + mfaToken: undefined, + mfaRecoveryCode: undefined, + }); + + expect(telemetry.track).toHaveBeenCalledWith('User attempted to login', { + result: 'success', + }); + + expect(router.push).toHaveBeenCalledWith('/home/workflows'); + }); }); From 069989c062cb4121deefcbededf36a72c66c6f15 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Tue, 29 Oct 2024 17:33:08 +0100 Subject: [PATCH 3/5] fix(editor): Auto focus first fields in Signup and ForgotMyPassword views --- .../src/views/ForgotMyPasswordView.test.ts | 15 ++++++++ .../src/views/ForgotMyPasswordView.vue | 1 + .../editor-ui/src/views/SignupView.test.ts | 35 +++++++++++++++++++ packages/editor-ui/src/views/SignupView.vue | 1 + 4 files changed, 52 insertions(+) create mode 100644 packages/editor-ui/src/views/ForgotMyPasswordView.test.ts create mode 100644 packages/editor-ui/src/views/SignupView.test.ts diff --git a/packages/editor-ui/src/views/ForgotMyPasswordView.test.ts b/packages/editor-ui/src/views/ForgotMyPasswordView.test.ts new file mode 100644 index 0000000000000..dcc0d22fb3eaf --- /dev/null +++ b/packages/editor-ui/src/views/ForgotMyPasswordView.test.ts @@ -0,0 +1,15 @@ +import { createComponentRenderer } from '@/__tests__/render'; +import { createTestingPinia } from '@pinia/testing'; +import ForgotMyPasswordView from '@/views/ForgotMyPasswordView.vue'; + +const renderComponent = createComponentRenderer(ForgotMyPasswordView); + +describe('ForgotMyPasswordView', () => { + beforeEach(() => { + createTestingPinia(); + }); + + it('should not throw error when opened', () => { + expect(() => renderComponent()).not.toThrow(); + }); +}); diff --git a/packages/editor-ui/src/views/ForgotMyPasswordView.vue b/packages/editor-ui/src/views/ForgotMyPasswordView.vue index 87aaa9dc47f3e..6d7d5c4881120 100644 --- a/packages/editor-ui/src/views/ForgotMyPasswordView.vue +++ b/packages/editor-ui/src/views/ForgotMyPasswordView.vue @@ -26,6 +26,7 @@ const formConfig = computed(() => { validationRules: [{ name: 'VALID_EMAIL' }], autocomplete: 'email', capitalize: true, + focusInitially: true, }, }, ]; diff --git a/packages/editor-ui/src/views/SignupView.test.ts b/packages/editor-ui/src/views/SignupView.test.ts new file mode 100644 index 0000000000000..0293292d99b50 --- /dev/null +++ b/packages/editor-ui/src/views/SignupView.test.ts @@ -0,0 +1,35 @@ +import { createComponentRenderer } from '@/__tests__/render'; +import { createTestingPinia } from '@pinia/testing'; +import SignupView from '@/views/SignupView.vue'; + +vi.mock('vue-router', () => { + const push = vi.fn(); + const replace = vi.fn(); + return { + useRouter: () => ({ + push, + replace, + }), + useRoute: () => ({ + query: { + inviterId: '123', + inviteeId: '456', + }, + }), + RouterLink: { + template: '', + }, + }; +}); + +const renderComponent = createComponentRenderer(SignupView); + +describe('SignupView', () => { + beforeEach(() => { + createTestingPinia(); + }); + + it('should not throw error when opened', () => { + expect(() => renderComponent()).not.toThrow(); + }); +}); diff --git a/packages/editor-ui/src/views/SignupView.vue b/packages/editor-ui/src/views/SignupView.vue index 3395616207f43..a68ba0a433bcd 100644 --- a/packages/editor-ui/src/views/SignupView.vue +++ b/packages/editor-ui/src/views/SignupView.vue @@ -30,6 +30,7 @@ const FORM_CONFIG: IFormBoxConfig = { required: true, autocomplete: 'given-name', capitalize: true, + focusInitially: true, }, }, { From c81bc234595df14443d1e2687031f3b9ce61a0cc Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 1 Nov 2024 10:07:59 +0100 Subject: [PATCH 4/5] test(editor): Add Signup unit tests --- .../editor-ui/src/views/SignupView.test.ts | 124 +++++++++++++++++- 1 file changed, 119 insertions(+), 5 deletions(-) diff --git a/packages/editor-ui/src/views/SignupView.test.ts b/packages/editor-ui/src/views/SignupView.test.ts index 0293292d99b50..e9eb69478f450 100644 --- a/packages/editor-ui/src/views/SignupView.test.ts +++ b/packages/editor-ui/src/views/SignupView.test.ts @@ -1,20 +1,24 @@ +import { useRoute, useRouter } from 'vue-router'; import { createComponentRenderer } from '@/__tests__/render'; import { createTestingPinia } from '@pinia/testing'; +import userEvent from '@testing-library/user-event'; +import { useToast } from '@/composables/useToast'; import SignupView from '@/views/SignupView.vue'; +import { VIEWS } from '@/constants'; +import { useUsersStore } from '@/stores/users.store'; +import { mockedStore } from '@/__tests__/utils'; vi.mock('vue-router', () => { const push = vi.fn(); const replace = vi.fn(); + const query = {}; return { useRouter: () => ({ push, replace, }), useRoute: () => ({ - query: { - inviterId: '123', - inviteeId: '456', - }, + query, }), RouterLink: { template: '', @@ -22,14 +26,124 @@ vi.mock('vue-router', () => { }; }); +vi.mock('@/composables/useToast', () => { + const showError = vi.fn(); + return { + useToast: () => ({ + showError, + }), + }; +}); + const renderComponent = createComponentRenderer(SignupView); +let route: ReturnType; +let router: ReturnType; +let toast: ReturnType; +let usersStore: ReturnType>; + describe('SignupView', () => { beforeEach(() => { + vi.clearAllMocks(); + createTestingPinia(); + + route = useRoute(); + router = useRouter(); + toast = useToast(); + + usersStore = mockedStore(useUsersStore); }); - it('should not throw error when opened', () => { + it('should not throw error when opened', async () => { expect(() => renderComponent()).not.toThrow(); }); + + it('should redirect to Signin when no inviterId and inviteeId', async () => { + renderComponent(); + + expect(toast.showError).toHaveBeenCalledWith(expect.any(Error), expect.any(String)); + expect(router.replace).toHaveBeenCalledWith({ name: VIEWS.SIGNIN }); + }); + + it('should validate signup token if there is any', async () => { + route.query.inviterId = '123'; + route.query.inviteeId = '456'; + + renderComponent(); + + expect(usersStore.validateSignupToken).toHaveBeenCalledWith({ + inviterId: '123', + inviteeId: '456', + }); + }); + + it('should not accept invitation when missing tokens', async () => { + const { getByRole } = renderComponent(); + + const acceptButton = getByRole('button', { name: 'Finish account setup' }); + + await userEvent.click(acceptButton); + + expect(toast.showError).toHaveBeenCalledWith(expect.any(Error), expect.any(String)); + expect(usersStore.acceptInvitation).not.toHaveBeenCalled(); + }); + + it('should not accept invitation when form is unfilled', async () => { + route.query.inviterId = '123'; + route.query.inviteeId = '456'; + + const { getByRole } = renderComponent(); + + const acceptButton = getByRole('button', { name: 'Finish account setup' }); + + await userEvent.click(acceptButton); + + expect(toast.showError).toHaveBeenCalledWith(expect.any(Error), expect.any(String)); + expect(usersStore.acceptInvitation).not.toHaveBeenCalled(); + }); + + it('should accept invitation with tokens', async () => { + route.query.inviterId = '123'; + route.query.inviteeId = '456'; + + usersStore.validateSignupToken.mockResolvedValueOnce({ + inviter: { + firstName: 'John', + lastName: 'Doe', + }, + }); + + const { getByRole, container } = renderComponent(); + + const acceptButton = getByRole('button', { name: 'Finish account setup' }); + + const firstNameInput = container.querySelector('input[name="firstName"]'); + const lastNameInput = container.querySelector('input[name="lastName"]'); + const passwordInput = container.querySelector('input[type="password"]'); + + if (!firstNameInput || !lastNameInput || !passwordInput) { + throw new Error('Inputs not found'); + } + + // TODO: Remove manual tabbing when the following issue is fixed (it should fail the test anyway) + // https://github.com/testing-library/vue-testing-library/issues/317 + await userEvent.tab(); + expect(document.activeElement).toBe(firstNameInput); + + await userEvent.type(firstNameInput, 'Jane'); + await userEvent.type(lastNameInput, 'Doe'); + await userEvent.type(passwordInput, '324R435gfg5fgj!'); + + await userEvent.click(acceptButton); + + expect(toast.showError).not.toHaveBeenCalled(); + expect(usersStore.acceptInvitation).toHaveBeenCalledWith({ + inviterId: '123', + inviteeId: '456', + firstName: 'Jane', + lastName: 'Doe', + password: '324R435gfg5fgj!', + }); + }); }); From dc14556e4ed108d3630fdf145e424e65f75822b4 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 1 Nov 2024 11:40:36 +0100 Subject: [PATCH 5/5] test(editor): Add Forgotpassword unit tests --- .../src/views/ForgotMyPasswordView.test.ts | 124 +++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/views/ForgotMyPasswordView.test.ts b/packages/editor-ui/src/views/ForgotMyPasswordView.test.ts index dcc0d22fb3eaf..6e5819c4406a8 100644 --- a/packages/editor-ui/src/views/ForgotMyPasswordView.test.ts +++ b/packages/editor-ui/src/views/ForgotMyPasswordView.test.ts @@ -1,15 +1,137 @@ import { createComponentRenderer } from '@/__tests__/render'; +import { mockedStore } from '@/__tests__/utils'; import { createTestingPinia } from '@pinia/testing'; +import userEvent from '@testing-library/user-event'; import ForgotMyPasswordView from '@/views/ForgotMyPasswordView.vue'; +import { useToast } from '@/composables/useToast'; +import { useUsersStore } from '@/stores/users.store'; +import { useSettingsStore } from '@/stores/settings.store'; -const renderComponent = createComponentRenderer(ForgotMyPasswordView); +vi.mock('vue-router', () => { + const push = vi.fn(); + const replace = vi.fn(); + const query = {}; + return { + useRouter: () => ({ + push, + replace, + }), + useRoute: () => ({ + query, + }), + RouterLink: { + template: '', + }, + }; +}); + +vi.mock('@/composables/useToast', () => { + const showError = vi.fn(); + const showMessage = vi.fn(); + return { + useToast: () => ({ + showError, + showMessage, + }), + }; +}); + +const renderComponent = createComponentRenderer(ForgotMyPasswordView, { + global: { + stubs: { + 'router-link': { + template: '', + }, + }, + }, +}); + +let toast: ReturnType; +let usersStore: ReturnType>; +let settingsStore: ReturnType>; describe('ForgotMyPasswordView', () => { beforeEach(() => { + vi.clearAllMocks(); + createTestingPinia(); + + toast = useToast(); + usersStore = mockedStore(useUsersStore); + settingsStore = mockedStore(useSettingsStore); }); it('should not throw error when opened', () => { expect(() => renderComponent()).not.toThrow(); }); + + it('should show email sending setup warning', async () => { + const { getByRole, queryByRole } = renderComponent(); + + const link = getByRole('link'); + const emailInput = queryByRole('textbox'); + + expect(emailInput).not.toBeInTheDocument(); + expect(link).toBeVisible(); + expect(link).toHaveTextContent('Back to sign in'); + }); + + it('should show form and submit', async () => { + settingsStore.isSmtpSetup = true; + usersStore.sendForgotPasswordEmail.mockResolvedValueOnce(); + + const { getByRole } = renderComponent(); + + const link = getByRole('link'); + const emailInput = getByRole('textbox'); + const submitButton = getByRole('button'); + + expect(emailInput).toBeVisible(); + expect(link).toBeVisible(); + expect(link).toHaveTextContent('Back to sign in'); + + // TODO: Remove manual tabbing when the following issue is fixed (it should fail the test anyway) + // https://github.com/testing-library/vue-testing-library/issues/317 + await userEvent.tab(); + expect(document.activeElement).toBe(emailInput); + + await userEvent.type(emailInput, 'test@n8n.io'); + await userEvent.click(submitButton); + + expect(usersStore.sendForgotPasswordEmail).toHaveBeenCalledWith({ + email: 'test@n8n.io', + }); + + expect(toast.showMessage).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.any(String), + message: expect.any(String), + }), + ); + }); + + it('should show form and error toast when submit has error', async () => { + settingsStore.isSmtpSetup = true; + usersStore.sendForgotPasswordEmail.mockRejectedValueOnce({ + httpStatusCode: 400, + }); + + const { getByRole } = renderComponent(); + + const emailInput = getByRole('textbox'); + const submitButton = getByRole('button'); + + await userEvent.type(emailInput, 'test@n8n.io'); + await userEvent.click(submitButton); + + expect(usersStore.sendForgotPasswordEmail).toHaveBeenCalledWith({ + email: 'test@n8n.io', + }); + + expect(toast.showMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'error', + }), + ); + }); });