diff --git a/contentcuration/contentcuration/frontend/accounts/components/form/StudioEmailField.vue b/contentcuration/contentcuration/frontend/accounts/components/form/StudioEmailField.vue new file mode 100644 index 0000000000..10b2c82127 --- /dev/null +++ b/contentcuration/contentcuration/frontend/accounts/components/form/StudioEmailField.vue @@ -0,0 +1,64 @@ + + + + diff --git a/contentcuration/contentcuration/frontend/accounts/components/form/StudioPasswordField.vue b/contentcuration/contentcuration/frontend/accounts/components/form/StudioPasswordField.vue new file mode 100644 index 0000000000..873b84ca11 --- /dev/null +++ b/contentcuration/contentcuration/frontend/accounts/components/form/StudioPasswordField.vue @@ -0,0 +1,48 @@ + + + + diff --git a/contentcuration/contentcuration/frontend/accounts/components/form/__tests__/StudioEmailField.spec.js b/contentcuration/contentcuration/frontend/accounts/components/form/__tests__/StudioEmailField.spec.js new file mode 100644 index 0000000000..5a770a55e1 --- /dev/null +++ b/contentcuration/contentcuration/frontend/accounts/components/form/__tests__/StudioEmailField.spec.js @@ -0,0 +1,66 @@ +import VueRouter from 'vue-router'; +import { render, screen, fireEvent } from '@testing-library/vue'; +import StudioEmailField from '../StudioEmailField.vue'; + +const renderComponent = (props = {}) => + render(StudioEmailField, { + router: new VueRouter(), + props: { + value: '', + ...props, + }, + }); + +describe('StudioEmailField', () => { + describe('rendering', () => { + it('renders with the default "Email address" label', () => { + renderComponent(); + expect(screen.getByLabelText(/email address/i)).toBeInTheDocument(); + }); + + it('renders with a custom label when provided', () => { + renderComponent({ label: 'Work email' }); + expect(screen.getByLabelText(/work email/i)).toBeInTheDocument(); + }); + + it('is disabled when the disabled prop is true', () => { + renderComponent({ disabled: true }); + expect(screen.getByLabelText(/email address/i)).toBeDisabled(); + }); + }); + + describe('input handling', () => { + it('emits trimmed value — strips leading and trailing whitespace', async () => { + const { emitted } = renderComponent(); + const input = screen.getByLabelText(/email address/i); + await fireEvent.update(input, ' test@example.com '); + expect(emitted().input).toBeTruthy(); + expect(emitted().input[0][0]).toBe('test@example.com'); + }); + + it('emits blur event when the field loses focus', async () => { + const { emitted } = renderComponent(); + const input = screen.getByLabelText(/email address/i); + await fireEvent.blur(input); + expect(emitted().blur).toBeTruthy(); + }); + }); + + describe('error display', () => { + it('shows the first error message when errorMessages is non-empty', () => { + renderComponent({ errorMessages: ['Please enter a valid email address'] }); + expect(screen.getByText('Please enter a valid email address')).toBeVisible(); + }); + + it('shows no error text when errorMessages is empty', () => { + renderComponent({ errorMessages: [] }); + expect(screen.queryByText('Please enter a valid email address')).not.toBeInTheDocument(); + }); + + it('shows only the first error when multiple messages are provided', () => { + renderComponent({ errorMessages: ['First error', 'Second error'] }); + expect(screen.getByText('First error')).toBeVisible(); + expect(screen.queryByText('Second error')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/accounts/components/form/__tests__/StudioPasswordField.spec.js b/contentcuration/contentcuration/frontend/accounts/components/form/__tests__/StudioPasswordField.spec.js new file mode 100644 index 0000000000..fabea6229a --- /dev/null +++ b/contentcuration/contentcuration/frontend/accounts/components/form/__tests__/StudioPasswordField.spec.js @@ -0,0 +1,63 @@ +import VueRouter from 'vue-router'; +import { render, screen, fireEvent } from '@testing-library/vue'; +import StudioPasswordField from '../StudioPasswordField.vue'; + +const renderComponent = (props = {}) => + render(StudioPasswordField, { + router: new VueRouter(), + props: { + value: '', + ...props, + }, + }); + +describe('StudioPasswordField', () => { + describe('rendering', () => { + it('renders with the default "Password" label', () => { + renderComponent(); + expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument(); + }); + + it('renders with a custom label when provided', () => { + renderComponent({ label: 'Confirm password' }); + expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument(); + }); + }); + + describe('input handling', () => { + it('emits raw value without trimming whitespace', async () => { + const { emitted } = renderComponent(); + const input = screen.getByLabelText(/^password$/i); + await fireEvent.update(input, ' mypassword '); + expect(emitted().input).toBeTruthy(); + expect(emitted().input[0][0]).toBe(' mypassword '); + }); + + it('emits blur event when the field loses focus', async () => { + const { emitted } = renderComponent(); + const input = screen.getByLabelText(/^password$/i); + await fireEvent.blur(input); + expect(emitted().blur).toBeTruthy(); + }); + }); + + describe('error display', () => { + it('shows the first error message when errorMessages is non-empty', () => { + renderComponent({ errorMessages: ['Password should be at least 8 characters long'] }); + expect(screen.getByText('Password should be at least 8 characters long')).toBeVisible(); + }); + + it('shows no error text when errorMessages is empty', () => { + renderComponent({ errorMessages: [] }); + expect( + screen.queryByText('Password should be at least 8 characters long'), + ).not.toBeInTheDocument(); + }); + + it('shows only the first error when multiple messages are provided', () => { + renderComponent({ errorMessages: ['First error', 'Second error'] }); + expect(screen.getByText('First error')).toBeVisible(); + expect(screen.queryByText('Second error')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/accounts/pages/Create.vue b/contentcuration/contentcuration/frontend/accounts/pages/Create.vue index 12074ef1ea..f1ad5d62bb 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/Create.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/Create.vue @@ -1,11 +1,13 @@