fix(editor): Auto focus first fields on SignIn, SignUp and ForgotMyPassword views (#11445)

This commit is contained in:
Csaba Tuncsik 2024-11-01 13:59:04 +01:00 committed by GitHub
parent 7d6dccbecb
commit 5b5bd7291d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 389 additions and 0 deletions

View 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',
}),
);
});
});

View file

@ -26,6 +26,7 @@ const formConfig = computed(() => {
validationRules: [{ name: 'VALID_EMAIL' }],
autocomplete: 'email',
capitalize: true,
focusInitially: true,
},
},
];

View 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');
});
});

View file

@ -60,6 +60,7 @@ const formConfig: IFormBoxConfig = reactive({
validateOnBlur: false,
autocomplete: 'email',
capitalize: true,
focusInitially: true,
},
},
{

View 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!',
});
});
});

View file

@ -30,6 +30,7 @@ const FORM_CONFIG: IFormBoxConfig = {
required: true,
autocomplete: 'given-name',
capitalize: true,
focusInitially: true,
},
},
{