@duffcloudservices/site-forms 0.1.0 → 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.
@@ -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
+ })
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import * as exports from '../index'
4
+
5
+ describe('site-forms entry', () => {
6
+ it('loads the shared stylesheet from the package entry', () => {
7
+ expect(exports.DcsForm).toBeTruthy()
8
+ })
9
+ })
@@ -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
+ })