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 00000000000..6e5819c4406
--- /dev/null
+++ b/packages/editor-ui/src/views/ForgotMyPasswordView.test.ts
@@ -0,0 +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';
+
+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',
+ }),
+ );
+ });
+});
diff --git a/packages/editor-ui/src/views/ForgotMyPasswordView.vue b/packages/editor-ui/src/views/ForgotMyPasswordView.vue
index 87aaa9dc47f..6d7d5c48811 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/SigninView.test.ts b/packages/editor-ui/src/views/SigninView.test.ts
new file mode 100644
index 00000000000..d1bbb9b020d
--- /dev/null
+++ b/packages/editor-ui/src/views/SigninView.test.ts
@@ -0,0 +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');
+ });
+});
diff --git a/packages/editor-ui/src/views/SigninView.vue b/packages/editor-ui/src/views/SigninView.vue
index fa258c5d7ad..f0c203d4694 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,
},
},
{
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 00000000000..e9eb69478f4
--- /dev/null
+++ b/packages/editor-ui/src/views/SignupView.test.ts
@@ -0,0 +1,149 @@
+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,
+ }),
+ RouterLink: {
+ template: '',
+ },
+ };
+});
+
+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', 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!',
+ });
+ });
+});
diff --git a/packages/editor-ui/src/views/SignupView.vue b/packages/editor-ui/src/views/SignupView.vue
index 3395616207f..a68ba0a433b 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,
},
},
{