Skip to content

Commit 81c7d19

Browse files
committed
Fix jobTitle and companyName required mismatch with action on Contact form
1 parent 3553db8 commit 81c7d19

7 files changed

Lines changed: 137 additions & 35 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
type ActionConfig<Input, Output> = {
4+
handler: (_input: Input, _context: unknown) => Promise<Output>
5+
}
6+
7+
const getMockedHandler = <Input, Output>(action: unknown): ActionConfig<Input, Output>['handler'] => {
8+
return (action as ActionConfig<Input, Output>).handler
9+
}
10+
11+
vi.mock('astro:actions', () => {
12+
return {
13+
defineAction: (config: unknown) => config,
14+
}
15+
})
16+
17+
vi.mock('@actions/gdpr/entities/consent', () => {
18+
return {
19+
createConsentRecord: vi.fn(async () => ({ id: 'consent-1' })),
20+
}
21+
})
22+
23+
vi.mock('@actions/utils/environment/environmentActions', () => {
24+
return {
25+
getPrivacyPolicyVersion: vi.fn(() => 'privacy-version-1'),
26+
}
27+
})
28+
29+
vi.mock('@actions/utils/errors', () => {
30+
return {
31+
handleActionsFunctionError: vi.fn(() => undefined),
32+
}
33+
})
34+
35+
vi.mock('@actions/utils/hubspot', () => {
36+
return {
37+
createOrUpdateContact: vi.fn(async () => ({ id: 'hubspot-1' })),
38+
setMarketingOptIn: vi.fn(async () => undefined),
39+
}
40+
})
41+
42+
beforeEach(() => {
43+
vi.clearAllMocks()
44+
})
45+
46+
describe('downloads inputSchema', () => {
47+
it('accepts omitted optional job fields', async () => {
48+
const { inputSchema } = await import('../action')
49+
50+
const result = inputSchema.parse({
51+
firstName: 'Jane',
52+
lastName: 'Doe',
53+
workEmail: 'jane@example.com',
54+
})
55+
56+
expect(result).toEqual({
57+
firstName: 'Jane',
58+
lastName: 'Doe',
59+
workEmail: 'jane@example.com',
60+
})
61+
})
62+
63+
it('normalizes blank optional job fields to undefined', async () => {
64+
const { inputSchema } = await import('../action')
65+
66+
const result = inputSchema.parse({
67+
firstName: 'Jane',
68+
lastName: 'Doe',
69+
workEmail: 'jane@example.com',
70+
jobTitle: ' ',
71+
companyName: '',
72+
})
73+
74+
expect(result).toEqual({
75+
firstName: 'Jane',
76+
lastName: 'Doe',
77+
workEmail: 'jane@example.com',
78+
jobTitle: undefined,
79+
companyName: undefined,
80+
})
81+
})
82+
})
83+
84+
describe('downloads.submit.handler', () => {
85+
it('submits successfully without optional job fields', async () => {
86+
const { downloads } = await import('../action')
87+
const { createOrUpdateContact } = await import('@actions/utils/hubspot')
88+
const { createConsentRecord } = await import('@actions/gdpr/entities/consent')
89+
90+
const context = {
91+
request: new Request('https://example.com/_actions/downloads/submit', {
92+
method: 'POST',
93+
headers: { 'user-agent': 'ua-1' },
94+
}),
95+
clientAddress: '203.0.113.10',
96+
}
97+
98+
const response = await getMockedHandler(downloads.submit)({
99+
firstName: 'Jane',
100+
lastName: 'Doe',
101+
workEmail: 'jane@example.com',
102+
}, context)
103+
104+
expect(response).toEqual({
105+
success: true,
106+
message: 'Form submitted successfully',
107+
})
108+
expect(createOrUpdateContact).toHaveBeenCalledWith({
109+
email: 'jane@example.com',
110+
firstname: 'Jane',
111+
lastname: 'Doe',
112+
})
113+
expect(createConsentRecord).not.toHaveBeenCalled()
114+
})
115+
})

‎src/actions/downloads/action.ts‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import { getPrivacyPolicyVersion } from '@actions/utils/environment/environmentA
77
import { handleActionsFunctionError } from '@actions/utils/errors'
88
import { createOrUpdateContact, setMarketingOptIn } from '@actions/utils/hubspot'
99

10+
const optionalTrimmedString = z.preprocess(value => {
11+
if (typeof value !== 'string') {
12+
return value
13+
}
14+
15+
const trimmedValue = value.trim()
16+
return trimmedValue.length > 0 ? trimmedValue : undefined
17+
}, z.string().optional())
18+
1019
export const inputSchema = z.object({
1120
firstName: z.string().trim().min(1),
1221
lastName: z.string().trim().min(1),
@@ -15,8 +24,8 @@ export const inputSchema = z.object({
1524
.trim()
1625
.min(1)
1726
.refine(value => emailValidator.validate(value), 'Invalid email address'),
18-
jobTitle: z.string().trim().min(1),
19-
companyName: z.string().trim().min(1),
27+
jobTitle: optionalTrimmedString,
28+
companyName: optionalTrimmedString,
2029
consent: z.boolean().optional(),
2130
DataSubjectId: z.uuid().optional(),
2231
})
@@ -59,8 +68,8 @@ export const downloads = {
5968
console.log('Download form submission:', {
6069
name: `${input.firstName} ${input.lastName}`,
6170
email: input.workEmail,
62-
jobTitle: input.jobTitle,
63-
company: input.companyName,
71+
jobTitle: input.jobTitle ?? null,
72+
company: input.companyName ?? null,
6473
timestamp: new Date().toISOString(),
6574
})
6675

‎src/components/Pages/Downloads/client/__tests__/__fixtures__/downloadForm.fixture.astro‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,6 @@ import Button from '@components/Button/index.astro'
1919
<input id="workEmail" name="workEmail" type="email" required />
2020
</div>
2121

22-
<div>
23-
<label for="jobTitle">Job Title</label>
24-
<input id="jobTitle" name="jobTitle" type="text" />
25-
</div>
26-
27-
<div>
28-
<label for="companyName">Company Name</label>
29-
<input id="companyName" name="companyName" type="text" />
30-
</div>
31-
3222
<Button id="downloadSubmitBtn" type="submit" text="Download Now" />
3323

3424
<div id="downloadButtonWrapper" class="hidden">

‎src/components/Pages/Downloads/client/__tests__/index.spec.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ const defaultFormValues = {
3737
firstName: 'Jane',
3838
lastName: 'Doe',
3939
workEmail: 'jane@example.com',
40-
jobTitle: 'Engineer',
41-
companyName: 'Acme Corp',
4240
}
4341

4442
const fillDownloadForm = (
@@ -49,8 +47,6 @@ const fillDownloadForm = (
4947
elements.firstName.value = values.firstName
5048
elements.lastName.value = values.lastName
5149
elements.workEmail.value = values.workEmail
52-
elements.jobTitle.value = values.jobTitle
53-
elements.companyName.value = values.companyName
5450
return values
5551
}
5652

@@ -93,7 +89,11 @@ describe('download-form web component', () => {
9389
submitForm(window, elements.form)
9490
await flushPromises()
9591

96-
expect(downloadsSubmitMock).toHaveBeenCalledWith(payload)
92+
expect(downloadsSubmitMock).toHaveBeenCalledWith({
93+
firstName: payload.firstName,
94+
lastName: payload.lastName,
95+
workEmail: payload.workEmail,
96+
})
9797
expect(markEmailCollectedMock).toHaveBeenCalledWith('jane@example.com', 'download_form')
9898
})
9999
})

‎src/components/Pages/Downloads/client/__tests__/testUtils.ts‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ import DownloadFormFixture from '@components/Pages/Downloads/client/__tests__/__
55
import type { DownloadFormElement } from '@components/Pages/Downloads/client'
66
import {
77
getDownloadButtonWrapper,
8-
getDownloadCompanyNameInput,
98
getDownloadFirstNameInput,
109
getDownloadFormElement,
11-
getDownloadJobTitleInput,
1210
getDownloadLastNameInput,
1311
getDownloadStatusDiv,
1412
getDownloadSubmitButton,
@@ -27,8 +25,6 @@ export interface DownloadFormElements {
2725
firstName: HTMLInputElement
2826
lastName: HTMLInputElement
2927
workEmail: HTMLInputElement
30-
jobTitle: HTMLInputElement
31-
companyName: HTMLInputElement
3228
}
3329

3430
export interface RenderDownloadFormContext {
@@ -67,8 +63,6 @@ export const renderDownloadForm = async (assertion: RenderDownloadFormAssertion)
6763
firstName: getDownloadFirstNameInput(window.document),
6864
lastName: getDownloadLastNameInput(window.document),
6965
workEmail: getDownloadWorkEmailInput(window.document),
70-
jobTitle: getDownloadJobTitleInput(window.document),
71-
companyName: getDownloadCompanyNameInput(window.document),
7266
},
7367
}
7468

‎src/components/Pages/Downloads/client/index.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,15 @@ export class DownloadFormElement extends LitElement {
114114
const dataSubjectId =
115115
typeof dataSubjectIdRaw === 'string' ? dataSubjectIdRaw.trim() : ''
116116
const DataSubjectId = dataSubjectId.length > 0 ? dataSubjectId : undefined
117+
const jobTitle = String(formData.get('jobTitle') ?? '').trim()
118+
const companyName = String(formData.get('companyName') ?? '').trim()
117119

118120
const payload = {
119121
firstName: String(formData.get('firstName') ?? ''),
120122
lastName: String(formData.get('lastName') ?? ''),
121123
workEmail: String(formData.get('workEmail') ?? ''),
122-
jobTitle: String(formData.get('jobTitle') ?? ''),
123-
companyName: String(formData.get('companyName') ?? ''),
124+
...(jobTitle && { jobTitle }),
125+
...(companyName && { companyName }),
124126
consent,
125127
DataSubjectId,
126128
} satisfies DownloadsSubmitInput

‎src/components/Pages/Downloads/client/selectors.ts‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,6 @@ export function getDownloadWorkEmailInput(root?: SelectorRoot): HTMLInputElement
9292
return queryInputElement('#workEmail', 'Work email input not found', root)
9393
}
9494

95-
export function getDownloadJobTitleInput(root?: SelectorRoot): HTMLInputElement {
96-
return queryInputElement('#jobTitle', 'Job title input not found', root)
97-
}
98-
99-
export function getDownloadCompanyNameInput(root?: SelectorRoot): HTMLInputElement {
100-
return queryInputElement('#companyName', 'Company name input not found', root)
101-
}
102-
10395
export type DownloadFormInvalidatableControl =
10496
| HTMLInputElement
10597
| HTMLSelectElement

0 commit comments

Comments
 (0)