mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 05:17:28 -08:00
fix(editor): Auto focus first fields on SignIn, SignUp and ForgotMyPassword views (#11445)
This commit is contained in:
parent
7d6dccbecb
commit
5b5bd7291d
137
packages/editor-ui/src/views/ForgotMyPasswordView.test.ts
Normal file
137
packages/editor-ui/src/views/ForgotMyPasswordView.test.ts
Normal file
|
@ -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: '<a><slot /></a>',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
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: '<a href="#"><slot /></a>',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let toast: ReturnType<typeof useToast>;
|
||||
let usersStore: ReturnType<typeof mockedStore<typeof useUsersStore>>;
|
||||
let settingsStore: ReturnType<typeof mockedStore<typeof useSettingsStore>>;
|
||||
|
||||
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',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -26,6 +26,7 @@ const formConfig = computed(() => {
|
|||
validationRules: [{ name: 'VALID_EMAIL' }],
|
||||
autocomplete: 'email',
|
||||
capitalize: true,
|
||||
focusInitially: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
100
packages/editor-ui/src/views/SigninView.test.ts
Normal file
100
packages/editor-ui/src/views/SigninView.test.ts
Normal file
|
@ -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: '<a><slot /></a>',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/composables/useTelemetry', () => {
|
||||
const track = vi.fn();
|
||||
return {
|
||||
useTelemetry: () => ({
|
||||
track,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const renderComponent = createComponentRenderer(SigninView);
|
||||
|
||||
let usersStore: ReturnType<typeof mockedStore<typeof useUsersStore>>;
|
||||
let settingsStore: ReturnType<typeof mockedStore<typeof useSettingsStore>>;
|
||||
|
||||
let router: ReturnType<typeof useRouter>;
|
||||
let telemetry: ReturnType<typeof useTelemetry>;
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
|
@ -60,6 +60,7 @@ const formConfig: IFormBoxConfig = reactive({
|
|||
validateOnBlur: false,
|
||||
autocomplete: 'email',
|
||||
capitalize: true,
|
||||
focusInitially: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
149
packages/editor-ui/src/views/SignupView.test.ts
Normal file
149
packages/editor-ui/src/views/SignupView.test.ts
Normal file
|
@ -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: '<a><slot /></a>',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/composables/useToast', () => {
|
||||
const showError = vi.fn();
|
||||
return {
|
||||
useToast: () => ({
|
||||
showError,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const renderComponent = createComponentRenderer(SignupView);
|
||||
|
||||
let route: ReturnType<typeof useRoute>;
|
||||
let router: ReturnType<typeof useRouter>;
|
||||
let toast: ReturnType<typeof useToast>;
|
||||
let usersStore: ReturnType<typeof mockedStore<typeof useUsersStore>>;
|
||||
|
||||
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!',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -30,6 +30,7 @@ const FORM_CONFIG: IFormBoxConfig = {
|
|||
required: true,
|
||||
autocomplete: 'given-name',
|
||||
capitalize: true,
|
||||
focusInitially: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue