Skip to content

Commit 44d4bb3

Browse files
[Remove Vuetify from Studio] 'Create an account' page (learningequality#5701)
* Add StudioEmailField and StudioPasswordField components * add custom validateForm method and replaced basic fields with new components and added emailValidateMessage translation * Refactor usage fields to use KCheckbox and KTextbox components, and implement toggleUsage method for form handling * updted Create.vue to replace VInput and TextArea with KSelect and KTextbox components, enhancing form handling and user experience * Replace Checkbox with KCheckbox component and update error message handling for agreement acceptance * remove vuetify from create page * [pre-commit.ci lite] apply automatic fixes * updated theme color error and fixed copilot review * test for email and password field * replace Vuetify components with KDS equivalents and update error handling text * updated lint error * [pre-commit.ci lite] apply automatic fixes * updated error handling in Create.vue * [pre-commit.ci lite] apply automatic fixes * fix max-len lint error in Create.vue comment * use generateFormMixin onSubmit/onValidationFailed pattern * add appearanceOverrides prop to form fields --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 4a63cf3 commit 44d4bb3

6 files changed

Lines changed: 639 additions & 327 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<template>
2+
3+
<KTextbox
4+
:value="value"
5+
type="email"
6+
:label="label || $tr('emailLabel')"
7+
:maxlength="maxlength"
8+
:disabled="disabled"
9+
:invalid="hasError"
10+
:invalidText="errorText"
11+
:showInvalidText="hasError"
12+
v-bind="$attrs"
13+
@input="handleInput"
14+
@blur="$emit('blur')"
15+
/>
16+
17+
</template>
18+
19+
20+
<script>
21+
22+
export default {
23+
name: 'StudioEmailField',
24+
props: {
25+
value: {
26+
type: String,
27+
default: '',
28+
},
29+
label: {
30+
type: String,
31+
default: null,
32+
},
33+
disabled: {
34+
type: Boolean,
35+
default: false,
36+
},
37+
errorMessages: {
38+
type: Array,
39+
default: () => [],
40+
},
41+
maxlength: {
42+
type: [String, Number],
43+
default: null,
44+
},
45+
},
46+
computed: {
47+
hasError() {
48+
return this.errorMessages && this.errorMessages.length > 0;
49+
},
50+
errorText() {
51+
return this.hasError ? this.errorMessages[0] : '';
52+
},
53+
},
54+
methods: {
55+
handleInput(value) {
56+
this.$emit('input', value.trim());
57+
},
58+
},
59+
$trs: {
60+
emailLabel: 'Email address',
61+
},
62+
};
63+
64+
</script>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<template>
2+
3+
<KTextbox
4+
:value="value"
5+
type="password"
6+
:label="label || $tr('passwordLabel')"
7+
:invalid="hasError"
8+
:invalidText="errorText"
9+
:showInvalidText="hasError"
10+
v-bind="$attrs"
11+
@input="$emit('input', $event)"
12+
@blur="$emit('blur')"
13+
/>
14+
15+
</template>
16+
17+
18+
<script>
19+
20+
export default {
21+
name: 'StudioPasswordField',
22+
props: {
23+
value: {
24+
type: String,
25+
default: '',
26+
},
27+
label: {
28+
type: String,
29+
default: null,
30+
},
31+
errorMessages: {
32+
type: Array,
33+
default: () => [],
34+
},
35+
},
36+
computed: {
37+
hasError() {
38+
return this.errorMessages && this.errorMessages.length > 0;
39+
},
40+
errorText() {
41+
return this.hasError ? this.errorMessages[0] : '';
42+
},
43+
},
44+
$trs: {
45+
passwordLabel: 'Password',
46+
},
47+
};
48+
49+
</script>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import VueRouter from 'vue-router';
2+
import { render, screen, fireEvent } from '@testing-library/vue';
3+
import StudioEmailField from '../StudioEmailField.vue';
4+
5+
const renderComponent = (props = {}) =>
6+
render(StudioEmailField, {
7+
router: new VueRouter(),
8+
props: {
9+
value: '',
10+
...props,
11+
},
12+
});
13+
14+
describe('StudioEmailField', () => {
15+
describe('rendering', () => {
16+
it('renders with the default "Email address" label', () => {
17+
renderComponent();
18+
expect(screen.getByLabelText(/email address/i)).toBeInTheDocument();
19+
});
20+
21+
it('renders with a custom label when provided', () => {
22+
renderComponent({ label: 'Work email' });
23+
expect(screen.getByLabelText(/work email/i)).toBeInTheDocument();
24+
});
25+
26+
it('is disabled when the disabled prop is true', () => {
27+
renderComponent({ disabled: true });
28+
expect(screen.getByLabelText(/email address/i)).toBeDisabled();
29+
});
30+
});
31+
32+
describe('input handling', () => {
33+
it('emits trimmed value — strips leading and trailing whitespace', async () => {
34+
const { emitted } = renderComponent();
35+
const input = screen.getByLabelText(/email address/i);
36+
await fireEvent.update(input, ' test@example.com ');
37+
expect(emitted().input).toBeTruthy();
38+
expect(emitted().input[0][0]).toBe('test@example.com');
39+
});
40+
41+
it('emits blur event when the field loses focus', async () => {
42+
const { emitted } = renderComponent();
43+
const input = screen.getByLabelText(/email address/i);
44+
await fireEvent.blur(input);
45+
expect(emitted().blur).toBeTruthy();
46+
});
47+
});
48+
49+
describe('error display', () => {
50+
it('shows the first error message when errorMessages is non-empty', () => {
51+
renderComponent({ errorMessages: ['Please enter a valid email address'] });
52+
expect(screen.getByText('Please enter a valid email address')).toBeVisible();
53+
});
54+
55+
it('shows no error text when errorMessages is empty', () => {
56+
renderComponent({ errorMessages: [] });
57+
expect(screen.queryByText('Please enter a valid email address')).not.toBeInTheDocument();
58+
});
59+
60+
it('shows only the first error when multiple messages are provided', () => {
61+
renderComponent({ errorMessages: ['First error', 'Second error'] });
62+
expect(screen.getByText('First error')).toBeVisible();
63+
expect(screen.queryByText('Second error')).not.toBeInTheDocument();
64+
});
65+
});
66+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import VueRouter from 'vue-router';
2+
import { render, screen, fireEvent } from '@testing-library/vue';
3+
import StudioPasswordField from '../StudioPasswordField.vue';
4+
5+
const renderComponent = (props = {}) =>
6+
render(StudioPasswordField, {
7+
router: new VueRouter(),
8+
props: {
9+
value: '',
10+
...props,
11+
},
12+
});
13+
14+
describe('StudioPasswordField', () => {
15+
describe('rendering', () => {
16+
it('renders with the default "Password" label', () => {
17+
renderComponent();
18+
expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument();
19+
});
20+
21+
it('renders with a custom label when provided', () => {
22+
renderComponent({ label: 'Confirm password' });
23+
expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument();
24+
});
25+
});
26+
27+
describe('input handling', () => {
28+
it('emits raw value without trimming whitespace', async () => {
29+
const { emitted } = renderComponent();
30+
const input = screen.getByLabelText(/^password$/i);
31+
await fireEvent.update(input, ' mypassword ');
32+
expect(emitted().input).toBeTruthy();
33+
expect(emitted().input[0][0]).toBe(' mypassword ');
34+
});
35+
36+
it('emits blur event when the field loses focus', async () => {
37+
const { emitted } = renderComponent();
38+
const input = screen.getByLabelText(/^password$/i);
39+
await fireEvent.blur(input);
40+
expect(emitted().blur).toBeTruthy();
41+
});
42+
});
43+
44+
describe('error display', () => {
45+
it('shows the first error message when errorMessages is non-empty', () => {
46+
renderComponent({ errorMessages: ['Password should be at least 8 characters long'] });
47+
expect(screen.getByText('Password should be at least 8 characters long')).toBeVisible();
48+
});
49+
50+
it('shows no error text when errorMessages is empty', () => {
51+
renderComponent({ errorMessages: [] });
52+
expect(
53+
screen.queryByText('Password should be at least 8 characters long'),
54+
).not.toBeInTheDocument();
55+
});
56+
57+
it('shows only the first error when multiple messages are provided', () => {
58+
renderComponent({ errorMessages: ['First error', 'Second error'] });
59+
expect(screen.getByText('First error')).toBeVisible();
60+
expect(screen.queryByText('Second error')).not.toBeInTheDocument();
61+
});
62+
});
63+
});

0 commit comments

Comments
 (0)