Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<template>

<KTextbox
:value="value"
type="email"
:label="label || $tr('emailLabel')"
:maxlength="maxlength"
:disabled="disabled"
:invalid="hasError"
:invalidText="errorText"
:showInvalidText="hasError"
v-bind="$attrs"
@input="handleInput"
@blur="$emit('blur')"
/>

</template>


<script>
export default {
name: 'StudioEmailField',
props: {
value: {
type: String,
default: '',
},
label: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
errorMessages: {
type: Array,
default: () => [],
},
maxlength: {
type: [String, Number],
default: null,
},
},
computed: {
hasError() {
return this.errorMessages && this.errorMessages.length > 0;
},
errorText() {
return this.hasError ? this.errorMessages[0] : '';
},
},
methods: {
handleInput(value) {
this.$emit('input', value.trim());
},
},
$trs: {
emailLabel: 'Email address',
},
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<template>

<KTextbox
:value="value"
type="password"
:label="label || $tr('passwordLabel')"
:invalid="hasError"
:invalidText="errorText"
:showInvalidText="hasError"
@input="$emit('input', $event)"
@blur="$emit('blur')"
/>

</template>


<script>

export default {
name: 'StudioPasswordField',
props: {
value: {
type: String,
default: '',
},
label: {
type: String,
default: null,
},
errorMessages: {
type: Array,
default: () => [],
},
},
computed: {
hasError() {
return this.errorMessages && this.errorMessages.length > 0;
},
errorText() {
return this.hasError ? this.errorMessages[0] : '';
},
},
$trs: {
passwordLabel: 'Password',
},
};

</script>
Original file line number Diff line number Diff line change
@@ -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');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Good test coverage — rendering, input handling (trimming), blur events, and error display are all covered using @testing-library/vue as the acceptance criteria require. The parallel StudioPasswordField tests similarly cover the raw (untrimmed) password input behavior, which is an important distinction.

});

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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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 ');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LianaHarris360 unless you already know, would you please be able to double-check on whether not trimming password is indeed expected and how it plays with backend? I would just like to be sure we won't cause any issues with passwords.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(also, the intention is use StudioPasswordField not only on Create account page, but we will also use it on Reset password page and in change password in Accounts)

});

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();
});
});
});
Loading