// Password control (UI-SPEC S7, D-19), the component on its own. No // application names appear in framework tests. import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { enableAutoUnmount, mount, type VueWrapper } from '@vue/test-utils' import { nextTick, ref } from 'vue' import type { FormField } from '../../src/api/types' import { setBundle } from '../../src/app/i18n' import PasswordField from '../../src/components/form/fields/PasswordField.vue' import { FORM_SESSION, type FormSession } from '../../src/components/form/formContext' import { langFixture } from '../fixtures/typed' import { resetState } from '../helpers' const strings = { 'backend::lang.form.show_password': { other: 'Show password' }, 'backend::lang.form.hide_password': { other: 'Hide password' }, } function mountField(props: Record = {}, revision = ref(0)) { const field = { name: 'password', type: 'password', label: 'Password', ...(props.field as object) } as FormField const wrapper = mount(PasswordField, { props: { modelValue: '', controlId: 'f-password', ...props, field }, global: { provide: { [FORM_SESSION as symbol]: { revision } as unknown as FormSession } }, attachTo: document.body, }) return { wrapper, revision } } const input = (wrapper: VueWrapper) => wrapper.find('input') const toggle = (wrapper: VueWrapper) => wrapper.find('[data-password-toggle]') beforeEach(() => { resetState() setBundle({ ...langFixture.data, ...strings }, 'en') }) afterEach(() => { document.body.innerHTML = '' }) enableAutoUnmount(afterEach) describe('password field component (UI-SPEC S7, D-19)', () => { it('is an empty, masked input that asks the browser for a new password', () => { const { wrapper } = mountField() expect(input(wrapper).attributes('type')).toBe('password') expect(input(wrapper).element.value).toBe('') expect(input(wrapper).attributes('id')).toBe('f-password') expect(input(wrapper).attributes('name')).toBe('password') expect(input(wrapper).attributes('autocomplete')).toBe('new-password') expect(input(wrapper).attributes('spellcheck')).toBe('false') }) it('never shows a value that is not a string', () => { for (const modelValue of [null, undefined, 12345, { hash: '$2a$10$abc' }, ['x']]) { const { wrapper } = mountField({ modelValue }) expect(input(wrapper).element.value).toBe('') wrapper.unmount() } }) it('emits what is typed and does not keep a copy of its own', async () => { const { wrapper } = mountField() await input(wrapper).setValue('s3cret-pass') expect(wrapper.emitted('update:modelValue')).toEqual([['s3cret-pass']]) // The control is driven by modelValue: the host clears it after a save. await wrapper.setProps({ modelValue: 's3cret-pass' }) await wrapper.setProps({ modelValue: '' }) expect(input(wrapper).element.value).toBe('') }) it('toggles between hidden and visible text and says so with aria-pressed', async () => { const { wrapper } = mountField({ modelValue: 'visible-text' }) expect(toggle(wrapper).attributes('type')).toBe('button') expect(toggle(wrapper).attributes('aria-pressed')).toBe('false') expect(toggle(wrapper).attributes('aria-label')).toBe('Show password') await toggle(wrapper).trigger('click') expect(input(wrapper).attributes('type')).toBe('text') expect(toggle(wrapper).attributes('aria-pressed')).toBe('true') expect(toggle(wrapper).attributes('aria-label')).toBe('Hide password') await toggle(wrapper).trigger('click') expect(input(wrapper).attributes('type')).toBe('password') expect(toggle(wrapper).attributes('aria-pressed')).toBe('false') // The toggle changes no value. expect(wrapper.emitted('update:modelValue')).toBeUndefined() }) it('returns to hidden when the form session moves on (after a save)', async () => { const { wrapper, revision } = mountField({ modelValue: 'visible-text' }) await toggle(wrapper).trigger('click') expect(input(wrapper).attributes('type')).toBe('text') revision.value += 1 await nextTick() expect(input(wrapper).attributes('type')).toBe('password') expect(toggle(wrapper).attributes('aria-pressed')).toBe('false') }) it('works without a form session', async () => { const wrapper = mount(PasswordField, { props: { field: { name: 'password', type: 'password' } as FormField, modelValue: '', controlId: 'x' }, }) await wrapper.find('[data-password-toggle]').trigger('click') expect(wrapper.find('input').attributes('type')).toBe('text') }) it('marks required and invalid, and points at its comment and error lines', () => { const { wrapper } = mountField({ field: { required: true }, invalid: true, describedBy: 'f-password-error' }) expect(input(wrapper).attributes('required')).toBeDefined() expect(input(wrapper).attributes('aria-required')).toBe('true') expect(input(wrapper).attributes('aria-invalid')).toBe('true') expect(input(wrapper).attributes('aria-describedby')).toBe('f-password-error') expect(input(wrapper).classes()).toContain('border-danger') const plain = mountField() expect(input(plain.wrapper).attributes('aria-invalid')).toBeUndefined() expect(input(plain.wrapper).attributes('required')).toBeUndefined() }) })