@duffcloudservices/site-forms 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -260
- package/dist/index.js.map +1 -1
- package/dist/site-forms.css +1 -0
- package/package.json +72 -73
- package/src/DcsForm.vue +303 -303
- package/src/__tests__/fields.test.ts +82 -82
- package/src/__tests__/multi-step.test.ts +46 -46
- package/src/__tests__/schema.test.ts +42 -42
- package/src/__tests__/style-import.test.ts +9 -0
- package/src/__tests__/submission.test.ts +77 -77
- package/src/__tests__/visible-if.test.ts +111 -111
- package/src/composables/useDcsForm.ts +201 -201
- package/src/composables/useFormSubmission.ts +113 -113
- package/src/composables/useFormValidation.ts +127 -127
- package/src/fields/DcsFormCheckbox.vue +35 -35
- package/src/fields/DcsFormCheckboxGroup.vue +52 -52
- package/src/fields/DcsFormDate.vue +34 -34
- package/src/fields/DcsFormFieldWrapper.vue +39 -39
- package/src/fields/DcsFormFile.vue +38 -38
- package/src/fields/DcsFormHidden.vue +17 -17
- package/src/fields/DcsFormHtmlBlock.vue +19 -19
- package/src/fields/DcsFormRadio.vue +45 -45
- package/src/fields/DcsFormSection.vue +19 -19
- package/src/fields/DcsFormSelect.vue +62 -62
- package/src/fields/DcsFormText.vue +54 -54
- package/src/fields/DcsFormTextarea.vue +43 -43
- package/src/index.ts +53 -51
- package/src/loaders/yaml.ts +51 -51
- package/src/schema/validate.ts +58 -58
- package/src/shims.d.ts +10 -10
- package/src/style.css +221 -0
- package/src/types.ts +140 -140
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { mount } from '@vue/test-utils'
|
|
3
|
-
import DcsForm from '../DcsForm.vue'
|
|
4
|
-
import type { PortalFormDefinition } from '../types'
|
|
5
|
-
|
|
6
|
-
function makeDef(
|
|
7
|
-
overrides: Partial<PortalFormDefinition> = {},
|
|
8
|
-
): PortalFormDefinition {
|
|
9
|
-
return {
|
|
10
|
-
formId: 'contact',
|
|
11
|
-
title: 'Contact',
|
|
12
|
-
submission: { kind: 'lead' },
|
|
13
|
-
fields: [
|
|
14
|
-
{ id: 'name', type: 'text', label: 'Name', required: true },
|
|
15
|
-
{ id: 'email', type: 'email', label: 'Email', required: true },
|
|
16
|
-
{ id: 'message', type: 'textarea', label: 'Message' },
|
|
17
|
-
],
|
|
18
|
-
...overrides,
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('field rendering', () => {
|
|
23
|
-
it('emits data-form-key on the form element', () => {
|
|
24
|
-
const wrapper = mount(DcsForm, {
|
|
25
|
-
props: { formId: 'contact', siteSlug: 's', definitionOverride: makeDef() },
|
|
26
|
-
})
|
|
27
|
-
const form = wrapper.find('form')
|
|
28
|
-
expect(form.attributes('data-form-key')).toBe('contact')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('renders an input per text field with data-form-field-key', () => {
|
|
32
|
-
const wrapper = mount(DcsForm, {
|
|
33
|
-
props: { formId: 'contact', siteSlug: 's', definitionOverride: makeDef() },
|
|
34
|
-
})
|
|
35
|
-
expect(wrapper.find('[data-form-field-key="name"] input[type="text"]').exists()).toBe(true)
|
|
36
|
-
expect(wrapper.find('[data-form-field-key="email"] input[type="email"]').exists()).toBe(true)
|
|
37
|
-
expect(wrapper.find('[data-form-field-key="message"] textarea').exists()).toBe(true)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('renders select / radio / checkbox-group / date / file / hidden / section / html-block', () => {
|
|
41
|
-
const def = makeDef({
|
|
42
|
-
fields: [
|
|
43
|
-
{
|
|
44
|
-
id: 'sel',
|
|
45
|
-
type: 'select',
|
|
46
|
-
label: 'Sel',
|
|
47
|
-
options: [{ value: 'a', label: 'A' }],
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: 'rad',
|
|
51
|
-
type: 'radio',
|
|
52
|
-
label: 'Rad',
|
|
53
|
-
options: [{ value: 'a', label: 'A' }],
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: 'chg',
|
|
57
|
-
type: 'checkbox-group',
|
|
58
|
-
label: 'Chg',
|
|
59
|
-
options: [{ value: 'a', label: 'A' }],
|
|
60
|
-
},
|
|
61
|
-
{ id: 'cb', type: 'checkbox', label: 'CB' },
|
|
62
|
-
{ id: 'dt', type: 'date', label: 'Dt' },
|
|
63
|
-
{ id: 'f', type: 'file', label: 'F' },
|
|
64
|
-
{ id: 'h', type: 'hidden', label: 'H', defaultValue: 'x' },
|
|
65
|
-
{ id: 'sec', type: 'section-heading', label: 'Sec' },
|
|
66
|
-
{ id: 'html', type: 'html-block', label: 'HTML', html: '<p>hi</p>' },
|
|
67
|
-
],
|
|
68
|
-
})
|
|
69
|
-
const wrapper = mount(DcsForm, {
|
|
70
|
-
props: { formId: 'contact', siteSlug: 's', definitionOverride: def },
|
|
71
|
-
})
|
|
72
|
-
expect(wrapper.find('[data-form-field-key="sel"] select').exists()).toBe(true)
|
|
73
|
-
expect(wrapper.find('[data-form-field-key="rad"] input[type="radio"]').exists()).toBe(true)
|
|
74
|
-
expect(wrapper.find('[data-form-field-key="chg"] input[type="checkbox"]').exists()).toBe(true)
|
|
75
|
-
expect(wrapper.find('[data-form-field-key="cb"] input[type="checkbox"]').exists()).toBe(true)
|
|
76
|
-
expect(wrapper.find('[data-form-field-key="dt"] input[type="date"]').exists()).toBe(true)
|
|
77
|
-
expect(wrapper.find('[data-form-field-key="f"] input[type="file"]').exists()).toBe(true)
|
|
78
|
-
expect(wrapper.find('input[type="hidden"][name="h"]').exists()).toBe(true)
|
|
79
|
-
expect(wrapper.find('[data-form-field-key="sec"] h3').exists()).toBe(true)
|
|
80
|
-
expect(wrapper.find('[data-form-field-key="html"]').html()).toContain('<p>hi</p>')
|
|
81
|
-
})
|
|
82
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import DcsForm from '../DcsForm.vue'
|
|
4
|
+
import type { PortalFormDefinition } from '../types'
|
|
5
|
+
|
|
6
|
+
function makeDef(
|
|
7
|
+
overrides: Partial<PortalFormDefinition> = {},
|
|
8
|
+
): PortalFormDefinition {
|
|
9
|
+
return {
|
|
10
|
+
formId: 'contact',
|
|
11
|
+
title: 'Contact',
|
|
12
|
+
submission: { kind: 'lead' },
|
|
13
|
+
fields: [
|
|
14
|
+
{ id: 'name', type: 'text', label: 'Name', required: true },
|
|
15
|
+
{ id: 'email', type: 'email', label: 'Email', required: true },
|
|
16
|
+
{ id: 'message', type: 'textarea', label: 'Message' },
|
|
17
|
+
],
|
|
18
|
+
...overrides,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('field rendering', () => {
|
|
23
|
+
it('emits data-form-key on the form element', () => {
|
|
24
|
+
const wrapper = mount(DcsForm, {
|
|
25
|
+
props: { formId: 'contact', siteSlug: 's', definitionOverride: makeDef() },
|
|
26
|
+
})
|
|
27
|
+
const form = wrapper.find('form')
|
|
28
|
+
expect(form.attributes('data-form-key')).toBe('contact')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('renders an input per text field with data-form-field-key', () => {
|
|
32
|
+
const wrapper = mount(DcsForm, {
|
|
33
|
+
props: { formId: 'contact', siteSlug: 's', definitionOverride: makeDef() },
|
|
34
|
+
})
|
|
35
|
+
expect(wrapper.find('[data-form-field-key="name"] input[type="text"]').exists()).toBe(true)
|
|
36
|
+
expect(wrapper.find('[data-form-field-key="email"] input[type="email"]').exists()).toBe(true)
|
|
37
|
+
expect(wrapper.find('[data-form-field-key="message"] textarea').exists()).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('renders select / radio / checkbox-group / date / file / hidden / section / html-block', () => {
|
|
41
|
+
const def = makeDef({
|
|
42
|
+
fields: [
|
|
43
|
+
{
|
|
44
|
+
id: 'sel',
|
|
45
|
+
type: 'select',
|
|
46
|
+
label: 'Sel',
|
|
47
|
+
options: [{ value: 'a', label: 'A' }],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'rad',
|
|
51
|
+
type: 'radio',
|
|
52
|
+
label: 'Rad',
|
|
53
|
+
options: [{ value: 'a', label: 'A' }],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'chg',
|
|
57
|
+
type: 'checkbox-group',
|
|
58
|
+
label: 'Chg',
|
|
59
|
+
options: [{ value: 'a', label: 'A' }],
|
|
60
|
+
},
|
|
61
|
+
{ id: 'cb', type: 'checkbox', label: 'CB' },
|
|
62
|
+
{ id: 'dt', type: 'date', label: 'Dt' },
|
|
63
|
+
{ id: 'f', type: 'file', label: 'F' },
|
|
64
|
+
{ id: 'h', type: 'hidden', label: 'H', defaultValue: 'x' },
|
|
65
|
+
{ id: 'sec', type: 'section-heading', label: 'Sec' },
|
|
66
|
+
{ id: 'html', type: 'html-block', label: 'HTML', html: '<p>hi</p>' },
|
|
67
|
+
],
|
|
68
|
+
})
|
|
69
|
+
const wrapper = mount(DcsForm, {
|
|
70
|
+
props: { formId: 'contact', siteSlug: 's', definitionOverride: def },
|
|
71
|
+
})
|
|
72
|
+
expect(wrapper.find('[data-form-field-key="sel"] select').exists()).toBe(true)
|
|
73
|
+
expect(wrapper.find('[data-form-field-key="rad"] input[type="radio"]').exists()).toBe(true)
|
|
74
|
+
expect(wrapper.find('[data-form-field-key="chg"] input[type="checkbox"]').exists()).toBe(true)
|
|
75
|
+
expect(wrapper.find('[data-form-field-key="cb"] input[type="checkbox"]').exists()).toBe(true)
|
|
76
|
+
expect(wrapper.find('[data-form-field-key="dt"] input[type="date"]').exists()).toBe(true)
|
|
77
|
+
expect(wrapper.find('[data-form-field-key="f"] input[type="file"]').exists()).toBe(true)
|
|
78
|
+
expect(wrapper.find('input[type="hidden"][name="h"]').exists()).toBe(true)
|
|
79
|
+
expect(wrapper.find('[data-form-field-key="sec"] h3').exists()).toBe(true)
|
|
80
|
+
expect(wrapper.find('[data-form-field-key="html"]').html()).toContain('<p>hi</p>')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { mount, flushPromises } from '@vue/test-utils'
|
|
3
|
-
import DcsForm from '../DcsForm.vue'
|
|
4
|
-
import type { PortalFormDefinition } from '../types'
|
|
5
|
-
|
|
6
|
-
const def: PortalFormDefinition = {
|
|
7
|
-
formId: 'wiz',
|
|
8
|
-
title: 'Wizard',
|
|
9
|
-
submission: { kind: 'lead' },
|
|
10
|
-
steps: [
|
|
11
|
-
{ id: 's1', title: 'One', fieldIds: ['a'] },
|
|
12
|
-
{ id: 's2', title: 'Two', fieldIds: ['b'] },
|
|
13
|
-
],
|
|
14
|
-
fields: [
|
|
15
|
-
{ id: 'a', type: 'text', label: 'A', required: true },
|
|
16
|
-
{ id: 'b', type: 'text', label: 'B', required: true },
|
|
17
|
-
],
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('multi-step navigation', () => {
|
|
21
|
-
it('starts on step 1, blocks Next until current step is valid', async () => {
|
|
22
|
-
const wrapper = mount(DcsForm, {
|
|
23
|
-
props: { formId: 'wiz', siteSlug: 's', definitionOverride: def },
|
|
24
|
-
})
|
|
25
|
-
expect(wrapper.text()).toContain('Step 1 of 2')
|
|
26
|
-
// Step 1 field B is not visible yet
|
|
27
|
-
expect(wrapper.find('[data-form-field-key="a"]').exists()).toBe(true)
|
|
28
|
-
expect(wrapper.find('[data-form-field-key="b"]').exists()).toBe(false)
|
|
29
|
-
|
|
30
|
-
// Click Next without filling A — should not advance
|
|
31
|
-
await wrapper.find('.dcs-form__btn--next').trigger('click')
|
|
32
|
-
expect(wrapper.text()).toContain('Step 1 of 2')
|
|
33
|
-
expect(wrapper.text()).toContain('A is required')
|
|
34
|
-
|
|
35
|
-
// Fill A, then advance
|
|
36
|
-
await wrapper.find('[data-form-field-key="a"] input').setValue('hi')
|
|
37
|
-
await wrapper.find('.dcs-form__btn--next').trigger('click')
|
|
38
|
-
await flushPromises()
|
|
39
|
-
expect(wrapper.text()).toContain('Step 2 of 2')
|
|
40
|
-
expect(wrapper.find('[data-form-field-key="b"]').exists()).toBe(true)
|
|
41
|
-
|
|
42
|
-
// Prev returns
|
|
43
|
-
await wrapper.find('.dcs-form__btn--prev').trigger('click')
|
|
44
|
-
expect(wrapper.text()).toContain('Step 1 of 2')
|
|
45
|
-
})
|
|
46
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount, flushPromises } from '@vue/test-utils'
|
|
3
|
+
import DcsForm from '../DcsForm.vue'
|
|
4
|
+
import type { PortalFormDefinition } from '../types'
|
|
5
|
+
|
|
6
|
+
const def: PortalFormDefinition = {
|
|
7
|
+
formId: 'wiz',
|
|
8
|
+
title: 'Wizard',
|
|
9
|
+
submission: { kind: 'lead' },
|
|
10
|
+
steps: [
|
|
11
|
+
{ id: 's1', title: 'One', fieldIds: ['a'] },
|
|
12
|
+
{ id: 's2', title: 'Two', fieldIds: ['b'] },
|
|
13
|
+
],
|
|
14
|
+
fields: [
|
|
15
|
+
{ id: 'a', type: 'text', label: 'A', required: true },
|
|
16
|
+
{ id: 'b', type: 'text', label: 'B', required: true },
|
|
17
|
+
],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('multi-step navigation', () => {
|
|
21
|
+
it('starts on step 1, blocks Next until current step is valid', async () => {
|
|
22
|
+
const wrapper = mount(DcsForm, {
|
|
23
|
+
props: { formId: 'wiz', siteSlug: 's', definitionOverride: def },
|
|
24
|
+
})
|
|
25
|
+
expect(wrapper.text()).toContain('Step 1 of 2')
|
|
26
|
+
// Step 1 field B is not visible yet
|
|
27
|
+
expect(wrapper.find('[data-form-field-key="a"]').exists()).toBe(true)
|
|
28
|
+
expect(wrapper.find('[data-form-field-key="b"]').exists()).toBe(false)
|
|
29
|
+
|
|
30
|
+
// Click Next without filling A — should not advance
|
|
31
|
+
await wrapper.find('.dcs-form__btn--next').trigger('click')
|
|
32
|
+
expect(wrapper.text()).toContain('Step 1 of 2')
|
|
33
|
+
expect(wrapper.text()).toContain('A is required')
|
|
34
|
+
|
|
35
|
+
// Fill A, then advance
|
|
36
|
+
await wrapper.find('[data-form-field-key="a"] input').setValue('hi')
|
|
37
|
+
await wrapper.find('.dcs-form__btn--next').trigger('click')
|
|
38
|
+
await flushPromises()
|
|
39
|
+
expect(wrapper.text()).toContain('Step 2 of 2')
|
|
40
|
+
expect(wrapper.find('[data-form-field-key="b"]').exists()).toBe(true)
|
|
41
|
+
|
|
42
|
+
// Prev returns
|
|
43
|
+
await wrapper.find('.dcs-form__btn--prev').trigger('click')
|
|
44
|
+
expect(wrapper.text()).toContain('Step 1 of 2')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { validateFormDefinition, warnIfInvalid } from '../schema/validate'
|
|
3
|
-
import type { PortalFormDefinition } from '../types'
|
|
4
|
-
|
|
5
|
-
const valid: PortalFormDefinition = {
|
|
6
|
-
formId: 'contact',
|
|
7
|
-
title: 'Contact',
|
|
8
|
-
submission: { kind: 'lead' },
|
|
9
|
-
fields: [
|
|
10
|
-
{ id: 'name', type: 'text', label: 'Name', required: true },
|
|
11
|
-
{ id: 'email', type: 'email', label: 'Email', required: true },
|
|
12
|
-
],
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('schema validate', () => {
|
|
16
|
-
it('passes for a well-formed definition', () => {
|
|
17
|
-
const r = validateFormDefinition(valid)
|
|
18
|
-
expect(r.valid).toBe(true)
|
|
19
|
-
expect(r.errors).toEqual([])
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('fails when required keys are missing', () => {
|
|
23
|
-
const bad = { formId: 'x' } as unknown as PortalFormDefinition
|
|
24
|
-
const r = validateFormDefinition(bad)
|
|
25
|
-
expect(r.valid).toBe(false)
|
|
26
|
-
expect(r.errors.length).toBeGreaterThan(0)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('emits a dev-only console.warn for malformed definitions', () => {
|
|
30
|
-
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
31
|
-
warnIfInvalid('contact', { formId: 'contact' }, true)
|
|
32
|
-
expect(spy).toHaveBeenCalled()
|
|
33
|
-
spy.mockRestore()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('does NOT warn when not in dev mode', () => {
|
|
37
|
-
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
38
|
-
warnIfInvalid('contact', { formId: 'contact' }, false)
|
|
39
|
-
expect(spy).not.toHaveBeenCalled()
|
|
40
|
-
spy.mockRestore()
|
|
41
|
-
})
|
|
42
|
-
})
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { validateFormDefinition, warnIfInvalid } from '../schema/validate'
|
|
3
|
+
import type { PortalFormDefinition } from '../types'
|
|
4
|
+
|
|
5
|
+
const valid: PortalFormDefinition = {
|
|
6
|
+
formId: 'contact',
|
|
7
|
+
title: 'Contact',
|
|
8
|
+
submission: { kind: 'lead' },
|
|
9
|
+
fields: [
|
|
10
|
+
{ id: 'name', type: 'text', label: 'Name', required: true },
|
|
11
|
+
{ id: 'email', type: 'email', label: 'Email', required: true },
|
|
12
|
+
],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('schema validate', () => {
|
|
16
|
+
it('passes for a well-formed definition', () => {
|
|
17
|
+
const r = validateFormDefinition(valid)
|
|
18
|
+
expect(r.valid).toBe(true)
|
|
19
|
+
expect(r.errors).toEqual([])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('fails when required keys are missing', () => {
|
|
23
|
+
const bad = { formId: 'x' } as unknown as PortalFormDefinition
|
|
24
|
+
const r = validateFormDefinition(bad)
|
|
25
|
+
expect(r.valid).toBe(false)
|
|
26
|
+
expect(r.errors.length).toBeGreaterThan(0)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('emits a dev-only console.warn for malformed definitions', () => {
|
|
30
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
31
|
+
warnIfInvalid('contact', { formId: 'contact' }, true)
|
|
32
|
+
expect(spy).toHaveBeenCalled()
|
|
33
|
+
spy.mockRestore()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('does NOT warn when not in dev mode', () => {
|
|
37
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
38
|
+
warnIfInvalid('contact', { formId: 'contact' }, false)
|
|
39
|
+
expect(spy).not.toHaveBeenCalled()
|
|
40
|
+
spy.mockRestore()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { mount, flushPromises } from '@vue/test-utils'
|
|
3
|
-
import DcsForm from '../DcsForm.vue'
|
|
4
|
-
import type { PortalFormDefinition } from '../types'
|
|
5
|
-
|
|
6
|
-
const def: PortalFormDefinition = {
|
|
7
|
-
formId: 'contact',
|
|
8
|
-
title: 'Contact',
|
|
9
|
-
submission: { kind: 'lead' },
|
|
10
|
-
fields: [
|
|
11
|
-
{ id: 'name', type: 'text', label: 'Name', required: true },
|
|
12
|
-
{ id: 'email', type: 'email', label: 'Email', required: true },
|
|
13
|
-
{ id: 'message', type: 'textarea', label: 'Message' },
|
|
14
|
-
],
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('submission happy path', () => {
|
|
18
|
-
it('POSTs the right payload shape and emits submit-success', async () => {
|
|
19
|
-
const fetchMock = vi.fn().mockResolvedValue({
|
|
20
|
-
ok: true,
|
|
21
|
-
status: 200,
|
|
22
|
-
json: async () => ({ leadId: 'L-1' }),
|
|
23
|
-
})
|
|
24
|
-
const originalFetch = globalThis.fetch
|
|
25
|
-
;(globalThis as unknown as { fetch: typeof fetch }).fetch =
|
|
26
|
-
fetchMock as unknown as typeof fetch
|
|
27
|
-
|
|
28
|
-
const wrapper = mount(DcsForm, {
|
|
29
|
-
props: {
|
|
30
|
-
formId: 'contact',
|
|
31
|
-
siteSlug: 'kept',
|
|
32
|
-
definitionOverride: def,
|
|
33
|
-
apiBase: 'https://api.example.com/',
|
|
34
|
-
captchaToken: 'tok-abc',
|
|
35
|
-
},
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
await wrapper.find('[data-form-field-key="name"] input').setValue('Jane')
|
|
39
|
-
await wrapper.find('[data-form-field-key="email"] input').setValue('jane@example.com')
|
|
40
|
-
await wrapper.find('[data-form-field-key="message"] textarea').setValue('hello')
|
|
41
|
-
|
|
42
|
-
await wrapper.find('form').trigger('submit')
|
|
43
|
-
await flushPromises()
|
|
44
|
-
|
|
45
|
-
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
46
|
-
const [url, init] = fetchMock.mock.calls[0]
|
|
47
|
-
expect(url).toBe('https://api.example.com/public/sites/kept/form-submissions')
|
|
48
|
-
expect((init as RequestInit).method).toBe('POST')
|
|
49
|
-
expect(((init as RequestInit).headers as Record<string, string>)['Content-Type']).toBe(
|
|
50
|
-
'application/json',
|
|
51
|
-
)
|
|
52
|
-
const body = JSON.parse((init as RequestInit).body as string)
|
|
53
|
-
expect(body).toEqual({
|
|
54
|
-
formId: 'contact',
|
|
55
|
-
values: { name: 'Jane', email: 'jane@example.com', message: 'hello' },
|
|
56
|
-
captchaToken: 'tok-abc',
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
expect(wrapper.emitted('submit-success')?.length).toBe(1)
|
|
60
|
-
|
|
61
|
-
;(globalThis as unknown as { fetch: typeof fetch }).fetch = originalFetch
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('emits validation-error when required fields are missing', async () => {
|
|
65
|
-
const wrapper = mount(DcsForm, {
|
|
66
|
-
props: {
|
|
67
|
-
formId: 'contact',
|
|
68
|
-
siteSlug: 'kept',
|
|
69
|
-
definitionOverride: def,
|
|
70
|
-
apiBase: 'https://api.example.com',
|
|
71
|
-
},
|
|
72
|
-
})
|
|
73
|
-
await wrapper.find('form').trigger('submit')
|
|
74
|
-
await flushPromises()
|
|
75
|
-
expect(wrapper.emitted('validation-error')?.length).toBe(1)
|
|
76
|
-
})
|
|
77
|
-
})
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { mount, flushPromises } from '@vue/test-utils'
|
|
3
|
+
import DcsForm from '../DcsForm.vue'
|
|
4
|
+
import type { PortalFormDefinition } from '../types'
|
|
5
|
+
|
|
6
|
+
const def: PortalFormDefinition = {
|
|
7
|
+
formId: 'contact',
|
|
8
|
+
title: 'Contact',
|
|
9
|
+
submission: { kind: 'lead' },
|
|
10
|
+
fields: [
|
|
11
|
+
{ id: 'name', type: 'text', label: 'Name', required: true },
|
|
12
|
+
{ id: 'email', type: 'email', label: 'Email', required: true },
|
|
13
|
+
{ id: 'message', type: 'textarea', label: 'Message' },
|
|
14
|
+
],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('submission happy path', () => {
|
|
18
|
+
it('POSTs the right payload shape and emits submit-success', async () => {
|
|
19
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
20
|
+
ok: true,
|
|
21
|
+
status: 200,
|
|
22
|
+
json: async () => ({ leadId: 'L-1' }),
|
|
23
|
+
})
|
|
24
|
+
const originalFetch = globalThis.fetch
|
|
25
|
+
;(globalThis as unknown as { fetch: typeof fetch }).fetch =
|
|
26
|
+
fetchMock as unknown as typeof fetch
|
|
27
|
+
|
|
28
|
+
const wrapper = mount(DcsForm, {
|
|
29
|
+
props: {
|
|
30
|
+
formId: 'contact',
|
|
31
|
+
siteSlug: 'kept',
|
|
32
|
+
definitionOverride: def,
|
|
33
|
+
apiBase: 'https://api.example.com/',
|
|
34
|
+
captchaToken: 'tok-abc',
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await wrapper.find('[data-form-field-key="name"] input').setValue('Jane')
|
|
39
|
+
await wrapper.find('[data-form-field-key="email"] input').setValue('jane@example.com')
|
|
40
|
+
await wrapper.find('[data-form-field-key="message"] textarea').setValue('hello')
|
|
41
|
+
|
|
42
|
+
await wrapper.find('form').trigger('submit')
|
|
43
|
+
await flushPromises()
|
|
44
|
+
|
|
45
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
46
|
+
const [url, init] = fetchMock.mock.calls[0]
|
|
47
|
+
expect(url).toBe('https://api.example.com/public/sites/kept/form-submissions')
|
|
48
|
+
expect((init as RequestInit).method).toBe('POST')
|
|
49
|
+
expect(((init as RequestInit).headers as Record<string, string>)['Content-Type']).toBe(
|
|
50
|
+
'application/json',
|
|
51
|
+
)
|
|
52
|
+
const body = JSON.parse((init as RequestInit).body as string)
|
|
53
|
+
expect(body).toEqual({
|
|
54
|
+
formId: 'contact',
|
|
55
|
+
values: { name: 'Jane', email: 'jane@example.com', message: 'hello' },
|
|
56
|
+
captchaToken: 'tok-abc',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
expect(wrapper.emitted('submit-success')?.length).toBe(1)
|
|
60
|
+
|
|
61
|
+
;(globalThis as unknown as { fetch: typeof fetch }).fetch = originalFetch
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('emits validation-error when required fields are missing', async () => {
|
|
65
|
+
const wrapper = mount(DcsForm, {
|
|
66
|
+
props: {
|
|
67
|
+
formId: 'contact',
|
|
68
|
+
siteSlug: 'kept',
|
|
69
|
+
definitionOverride: def,
|
|
70
|
+
apiBase: 'https://api.example.com',
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
await wrapper.find('form').trigger('submit')
|
|
74
|
+
await flushPromises()
|
|
75
|
+
expect(wrapper.emitted('validation-error')?.length).toBe(1)
|
|
76
|
+
})
|
|
77
|
+
})
|