- MLField.vue is the one locale-switching wrapper; MLTextField, MLMarkdownField and the new MLTextareaField are shells around it - isMLFieldType replaces the inline ML type-name checks in formState, FormView and RelationChildModal - register mltextarea as a group-labelled control; rebuild dist
276 lines
11 KiB
TypeScript
276 lines
11 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
import { ref } from 'vue'
|
|
import type { FormField } from '../../src/api/types'
|
|
import MarkdownField from '../../src/components/form/fields/MarkdownField.vue'
|
|
import MLMarkdownField from '../../src/components/form/fields/MLMarkdownField.vue'
|
|
import MLTextareaField from '../../src/components/form/fields/MLTextareaField.vue'
|
|
import MLTextField from '../../src/components/form/fields/MLTextField.vue'
|
|
import { FORM_ENABLED_LOCALES } from '../../src/components/form/formContext'
|
|
import { editablePayload, initialValues, isMLFieldType, mergeMLValue } from '../../src/components/form/formState'
|
|
import { isRegistered, rendererFor } from '../../src/components/form/registry'
|
|
import { API, mockApi, resetState } from '../helpers'
|
|
|
|
function field(type: string, name = 'title'): FormField {
|
|
return { name, label: 'Title', type } as FormField
|
|
}
|
|
|
|
const enabledLocales = ref<readonly string[]>(['en', 'pl'])
|
|
|
|
function mlMount(component: typeof MLTextField | typeof MLMarkdownField | typeof MLTextareaField, props: object) {
|
|
return mount(component, {
|
|
props: props as never,
|
|
attachTo: document.body,
|
|
global: {
|
|
provide: {
|
|
[FORM_ENABLED_LOCALES as symbol]: enabledLocales,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetState()
|
|
enabledLocales.value = ['en', 'pl']
|
|
})
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = ''
|
|
})
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
describe('ML field registry and nested save body', () => {
|
|
it('resolves markdown, mltext and mlmarkdown', () => {
|
|
expect(rendererFor('markdown')).toBe(MarkdownField)
|
|
expect(rendererFor('mltext')).toBe(MLTextField)
|
|
expect(rendererFor('mlmarkdown')).toBe(MLMarkdownField)
|
|
expect(isRegistered('markdown')).toBe(true)
|
|
expect(isRegistered('mltext')).toBe(true)
|
|
expect(isRegistered('mlmarkdown')).toBe(true)
|
|
})
|
|
|
|
it('sends every locale as a nested record', () => {
|
|
const fields = [field('mltext'), field('mlmarkdown', 'body')]
|
|
const payload = editablePayload(fields, {
|
|
title: { en: 'Hello', pl: 'Witaj' },
|
|
body: { en: '# Hi', pl: '# Cześć' },
|
|
})
|
|
expect(payload).toEqual({
|
|
title: { en: 'Hello', pl: 'Witaj' },
|
|
body: { en: '# Hi', pl: '# Cześć' },
|
|
})
|
|
})
|
|
|
|
it('sends every enabled locale including empty pl', () => {
|
|
const fields = [field('mltext'), field('mlmarkdown', 'body')]
|
|
const payload = editablePayload(fields, {
|
|
title: { en: 'Hello', pl: '' },
|
|
body: { en: '# Hi', pl: '' },
|
|
})
|
|
expect(payload).toEqual({
|
|
title: { en: 'Hello', pl: '' },
|
|
body: { en: '# Hi', pl: '' },
|
|
})
|
|
})
|
|
|
|
it('exposes one locale selector per ML field listing enabled locales', () => {
|
|
const title = mlMount(MLTextField, {
|
|
field: field('mltext'),
|
|
modelValue: { en: '', pl: '' },
|
|
controlId: 'f-title',
|
|
})
|
|
const body = mlMount(MLMarkdownField, {
|
|
field: field('mlmarkdown', 'body'),
|
|
modelValue: { en: '', pl: '' },
|
|
controlId: 'f-body',
|
|
})
|
|
expect(title.findAll('[data-ml-locale]')).toHaveLength(1)
|
|
expect(body.findAll('[data-ml-locale]')).toHaveLength(1)
|
|
expect(title.find('[data-ml-copy-from]').exists()).toBe(false)
|
|
expect(body.find('[data-ml-copy-from]').exists()).toBe(false)
|
|
expect(title.find('[data-ml-copy]').exists()).toBe(false)
|
|
expect(body.find('[data-ml-copy]').exists()).toBe(false)
|
|
expect(title.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
|
|
expect(body.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
|
|
})
|
|
|
|
it('edits the active locale without overwriting sibling locales', async () => {
|
|
const wrapper = mlMount(MLTextField, {
|
|
field: field('mltext'),
|
|
modelValue: { en: 'Hello', pl: 'Witaj' },
|
|
controlId: 'f-title',
|
|
})
|
|
const input = wrapper.find('input')
|
|
expect(input.element.value).toBe('Hello')
|
|
await wrapper.find('[data-ml-locale]').setValue('pl')
|
|
expect(wrapper.find('input').element.value).toBe('Witaj')
|
|
await wrapper.find('input').setValue('Cześć')
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: 'Hello', pl: 'Cześć' }])
|
|
})
|
|
|
|
it('switches every ML control from one selector', async () => {
|
|
const title = mlMount(MLTextField, {
|
|
field: field('mltext'),
|
|
modelValue: { en: 'Hello', pl: 'Witaj' },
|
|
controlId: 'f-title',
|
|
})
|
|
const body = mlMount(MLMarkdownField, {
|
|
field: field('mlmarkdown', 'body'),
|
|
modelValue: { en: 'EN', pl: 'PL' },
|
|
controlId: 'f-body',
|
|
})
|
|
expect(body.findComponent(MarkdownField).exists()).toBe(true)
|
|
await title.find('[data-ml-locale]').setValue('pl')
|
|
expect((body.find('[data-ml-locale]').element as HTMLSelectElement).value).toBe('pl')
|
|
expect(body.find('textarea').element.value).toBe('PL')
|
|
})
|
|
|
|
it('does not execute raw HTML in the markdown preview', async () => {
|
|
mockApi({ [`POST ${API}/markdown/preview`]: { body: { data: { html: '<p><!-- raw HTML omitted --></p>\n' }, meta: {} } } })
|
|
const wrapper = mount(MarkdownField, {
|
|
props: {
|
|
field: field('markdown', 'body'),
|
|
modelValue: '<script>window.__ml_xss = 1</script><img src=x onerror="window.__ml_xss = 1">',
|
|
controlId: 'f-body',
|
|
},
|
|
attachTo: document.body,
|
|
})
|
|
await wrapper.find('[data-markdown-preview]').trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper.find('script').exists()).toBe(false)
|
|
expect(wrapper.find('img').exists()).toBe(false)
|
|
expect((window as unknown as { __ml_xss?: number }).__ml_xss).toBeUndefined()
|
|
})
|
|
|
|
it('seeds empty maps and lists every enabled locale on create', () => {
|
|
const fields = [field('mltext')]
|
|
const values = initialValues(fields, ['en', 'pl'])
|
|
expect(values.title).toEqual({ en: '', pl: '' })
|
|
const wrapper = mlMount(MLTextField, {
|
|
field: fields[0],
|
|
modelValue: values.title,
|
|
controlId: 'f-title',
|
|
})
|
|
expect(wrapper.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
|
|
})
|
|
|
|
it('keeps sibling locales when adopt merges a GET host string', async () => {
|
|
const fields = [field('mltext')]
|
|
const merged = mergeMLValue('Hello', ['en', 'pl'])
|
|
expect(merged).toEqual({ en: 'Hello', pl: '' })
|
|
const wrapper = mlMount(MLTextField, {
|
|
field: fields[0],
|
|
modelValue: merged,
|
|
controlId: 'f-title',
|
|
})
|
|
expect(wrapper.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
|
|
await wrapper.find('[data-ml-locale]').setValue('pl')
|
|
await wrapper.find('input').setValue('Witaj')
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: 'Hello', pl: 'Witaj' }])
|
|
})
|
|
|
|
it('seeds empty maps and lists every enabled locale on mlmarkdown create', () => {
|
|
const fields = [field('mlmarkdown', 'body')]
|
|
const values = initialValues(fields, ['en', 'pl'])
|
|
expect(values.body).toEqual({ en: '', pl: '' })
|
|
const wrapper = mlMount(MLMarkdownField, {
|
|
field: fields[0],
|
|
modelValue: values.body,
|
|
controlId: 'f-body',
|
|
})
|
|
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true)
|
|
expect(wrapper.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
|
|
})
|
|
|
|
it('keeps sibling locales when adopt merges a GET host string on mlmarkdown', async () => {
|
|
const fields = [field('mlmarkdown', 'body')]
|
|
const merged = mergeMLValue('# Hello', ['en', 'pl'])
|
|
expect(merged).toEqual({ en: '# Hello', pl: '' })
|
|
const wrapper = mlMount(MLMarkdownField, {
|
|
field: fields[0],
|
|
modelValue: merged,
|
|
controlId: 'f-body',
|
|
})
|
|
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true)
|
|
expect(wrapper.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
|
|
await wrapper.find('[data-ml-locale]').setValue('pl')
|
|
await wrapper.find('textarea').setValue('# Witaj')
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: '# Hello', pl: '# Witaj' }])
|
|
})
|
|
})
|
|
|
|
describe('mltextarea field', () => {
|
|
function excerpt(size?: string): FormField {
|
|
return { name: 'excerpt', label: 'Excerpt', type: 'mltextarea', ...(size ? { size } : {}) } as FormField
|
|
}
|
|
|
|
it('treats mltext, mltextarea and mlmarkdown as ML field types', () => {
|
|
expect(isMLFieldType('mltext')).toBe(true)
|
|
expect(isMLFieldType('mltextarea')).toBe(true)
|
|
expect(isMLFieldType('mlmarkdown')).toBe(true)
|
|
expect(isMLFieldType('text')).toBe(false)
|
|
expect(isMLFieldType('textarea')).toBe(false)
|
|
expect(isMLFieldType('markdown')).toBe(false)
|
|
expect(isMLFieldType(undefined)).toBe(false)
|
|
})
|
|
|
|
it('sends an mltextarea value as its locale record', () => {
|
|
const payload = editablePayload([excerpt('small')], {
|
|
excerpt: { en: 'Line 1\nLine 2', pl: 'Linia', de: 3 },
|
|
})
|
|
expect(payload).toEqual({ excerpt: { en: 'Line 1\nLine 2', pl: 'Linia' } })
|
|
})
|
|
|
|
it('seeds every enabled locale on create', () => {
|
|
const values = initialValues([excerpt('small')], ['en', 'pl'])
|
|
expect(values.excerpt).toEqual({ en: '', pl: '' })
|
|
})
|
|
|
|
it('renders a sized textarea with one locale select and edits the active locale', async () => {
|
|
const wrapper = mlMount(MLTextareaField, {
|
|
field: excerpt('small'),
|
|
modelValue: { en: 'Line 1\nLine 2', pl: 'Linia' },
|
|
controlId: 'f-excerpt',
|
|
})
|
|
const root = wrapper.find('[data-ml-field]')
|
|
expect(root.attributes('data-ml-field')).toBe('mltextarea')
|
|
expect(root.attributes('role')).toBe('group')
|
|
expect(wrapper.findAll('[data-ml-locale]')).toHaveLength(1)
|
|
expect(wrapper.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
|
|
expect(wrapper.find('input').exists()).toBe(false)
|
|
const textarea = wrapper.find('textarea')
|
|
expect(textarea.attributes('rows')).toBe('4')
|
|
expect(textarea.element.value).toBe('Line 1\nLine 2')
|
|
await wrapper.find('[data-ml-locale]').setValue('pl')
|
|
expect(wrapper.find('textarea').element.value).toBe('Linia')
|
|
await wrapper.find('textarea').setValue('Nowa\nlinia')
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: 'Line 1\nLine 2', pl: 'Nowa\nlinia' }])
|
|
})
|
|
|
|
it('uses the default rows without a size', () => {
|
|
const wrapper = mlMount(MLTextareaField, {
|
|
field: excerpt(),
|
|
modelValue: { en: '', pl: '' },
|
|
controlId: 'f-excerpt',
|
|
})
|
|
expect(wrapper.find('textarea').attributes('rows')).toBe('5')
|
|
})
|
|
|
|
it('follows the locale chosen on another ML control', async () => {
|
|
const title = mlMount(MLTextField, {
|
|
field: field('mltext'),
|
|
modelValue: { en: 'Hello', pl: 'Witaj' },
|
|
controlId: 'f-title',
|
|
})
|
|
const body = mlMount(MLTextareaField, {
|
|
field: excerpt('small'),
|
|
modelValue: { en: 'EN', pl: 'PL' },
|
|
controlId: 'f-excerpt',
|
|
})
|
|
await title.find('[data-ml-locale]').setValue('pl')
|
|
expect((body.find('[data-ml-locale]').element as HTMLSelectElement).value).toBe('pl')
|
|
expect(body.find('textarea').element.value).toBe('PL')
|
|
})
|
|
})
|