Files
summercms/admin/tests/form/MLFields.test.ts
Jakub Zych ef03016314 test(14.2.1-06): land remaining named tests and phase gate
WR-02 proofs and T-14.2.1-19..23 must fail the phase closed if hydration or child locale validation is removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 16:28:29 +02:00

197 lines
7.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { enableAutoUnmount, 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 MLTextField from '../../src/components/form/fields/MLTextField.vue'
import { FORM_ENABLED_LOCALES } from '../../src/components/form/formContext'
import { editablePayload, initialValues, mergeMLValue } from '../../src/components/form/formState'
import { isRegistered, rendererFor } from '../../src/components/form/registry'
import { 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, 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.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 and copies from another', 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('[data-ml-copy-from]').setValue('en')
await wrapper.find('[data-ml-copy]').trigger('click')
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: 'Hello', pl: 'Hello' }])
})
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 () => {
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')
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()
expect(wrapper.find('[data-markdown-preview-pane]').text()).toContain('<script>')
})
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' }])
})
})