Files
summercms/admin/tests/form/PasswordField.test.ts
Jakub Zych 84efdc09e0 test(12.1-05): unit tests for the Phase 12.1 SPA components
- BulkActionsMenu, RowStateBadges, RecordActions, PreviewField, FormErrorBanner, PasswordField and PermissionEditorField, each mounted on its own
- backstop: a row state outside the fixed set renders no badge and no class
- backstop: the permission editor's emission rules per mode, the locked row and codes outside the options
2026-10-05 15:29:44 +02:00

117 lines
5.2 KiB
TypeScript

// 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<string, unknown> = {}, 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<HTMLInputElement>('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()
})
})