Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get Contact page question labels and descriptions from back-end #744

Merged
merged 20 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/public/src/app/(general)/Home.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const mockFormData = [
inputType: 'text',
showCharCount: false,
position: 0,
maxCharCount: 0,
autoExpand: false,
},
]

Expand Down
7 changes: 3 additions & 4 deletions apps/public/src/app/(general)/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
'use client'

import { Paragraph } from '@amsterdam/design-system-react'
import type { StaticFormPanelComponentOutput, StaticFormTextFieldInputComponentOutput } from '@meldingen/api-client'
import { FormRenderer } from '@meldingen/form-renderer'
import { Grid } from '@meldingen/ui'
import { useTranslations } from 'next-intl'
import { useActionState } from 'react'

import { postPrimaryForm } from './actions'
import type { StaticFormTextAreaComponentOutput } from 'apps/public/src/apiClientProxy'

type Component = StaticFormPanelComponentOutput | StaticFormTextFieldInputComponentOutput
import { postPrimaryForm } from './actions'

const initialState: { message?: string } = {}

export const Home = ({ formData }: { formData: Component[] }) => {
export const Home = ({ formData }: { formData: StaticFormTextAreaComponentOutput[] }) => {
const [formState, formAction] = useActionState(postPrimaryForm, initialState)

const t = useTranslations('homepage')
Expand Down
66 changes: 62 additions & 4 deletions apps/public/src/app/(general)/contact/Contact.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,82 @@ vi.mock('react', async (importOriginal) => {
}
})

const mockFormData = [
{
type: 'textarea',
label: 'Wat is uw e-mailadres?',
description: 'Test 1',
key: 'email',
input: true,
inputType: 'text',
maxCharCount: 0,
position: 0,
autoExpand: false,
},
{
type: 'textarea',
label: 'Wat is uw telefoonnummer?',
description: 'Test 2',
key: 'tel',
input: true,
inputType: 'text',
maxCharCount: 0,
position: 1,
autoExpand: false,
},
]

describe('Contact', () => {
it('should render page and form', async () => {
render(<Contact />)
render(<Contact formData={mockFormData} />)

const emailInput = screen.getByRole('textbox', { name: 'Wat is uw e-mailadres? (niet verplicht)' })
const phoneInput = screen.getByRole('textbox', { name: 'Wat is uw telefoonnummer? (niet verplicht)' })
const telInput = screen.getByRole('textbox', { name: 'Wat is uw telefoonnummer? (niet verplicht)' })
const submitButton = screen.getByRole('button')

expect(emailInput).toBeInTheDocument()
expect(phoneInput).toBeInTheDocument()
expect(telInput).toBeInTheDocument()
expect(submitButton).toBeInTheDocument()
})

it('should render an error message', () => {
;(useActionState as Mock).mockReturnValue([{ message: 'Test error message' }, vi.fn()])

render(<Contact />)
render(<Contact formData={mockFormData} />)

expect(screen.queryByText('Test error message')).toBeInTheDocument()
})

it('should render descriptions that are connected to the e-mail and tel inputs', () => {
render(<Contact formData={mockFormData} />)

const emailInput = screen.getByRole('textbox', { name: 'Wat is uw e-mailadres? (niet verplicht)' })
const telInput = screen.getByRole('textbox', { name: 'Wat is uw telefoonnummer? (niet verplicht)' })

expect(emailInput).toHaveAccessibleDescription('Test 1')
expect(telInput).toHaveAccessibleDescription('Test 2')
})

it('should not render descriptions when they are not provided', () => {
const mockFormDataWithoutDescriptions = [
{
...mockFormData[0],
description: '',
},
{
...mockFormData[1],
description: '',
},
]

render(<Contact formData={mockFormDataWithoutDescriptions} />)

const emailInput = screen.getByRole('textbox', { name: 'Wat is uw e-mailadres? (niet verplicht)' })
const telInput = screen.getByRole('textbox', { name: 'Wat is uw telefoonnummer? (niet verplicht)' })

expect(emailInput).not.toHaveAccessibleDescription()
expect(emailInput).not.toHaveAttribute('aria-describedby', 'email-input-description')
expect(telInput).not.toHaveAccessibleDescription()
expect(telInput).not.toHaveAttribute('aria-describedby', 'tel-input-description')
})
})
33 changes: 29 additions & 4 deletions apps/public/src/app/(general)/contact/Contact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ import { Alert, Heading, Label, Paragraph, TextInput } from '@amsterdam/design-s
import { Grid, SubmitButton } from '@meldingen/ui'
import { useActionState } from 'react'

Comment on lines 4 to 6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't set a test coverage limit, but branches of this component are at 77% since the airadescribedby is not tested.

import type { StaticFormTextAreaComponentOutput } from 'apps/public/src/apiClientProxy'

import { BackLink } from '../_components/BackLink'

import { postContactForm } from './actions'

const initialState: { message?: string } = {}

export const Contact = () => {
export const Contact = ({ formData }: { formData: StaticFormTextAreaComponentOutput[] }) => {
const [formState, formAction] = useActionState(postContactForm, initialState)

const emailLabel = formData[0].label
const emailDescription = formData[0].description
const telLabel = formData[1].label
const telDescription = formData[1].description

return (
<Grid paddingBottom="large" paddingTop="medium">
<Grid.Cell span={{ narrow: 4, medium: 6, wide: 6 }} start={{ narrow: 1, medium: 2, wide: 3 }}>
Comment on lines 23 to 25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component needs internationalisation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. I didn't do that yet to prevent merge conflicts.

Expand All @@ -39,9 +46,15 @@ export const Contact = () => {
</Alert>
)}
<Label htmlFor="email-input" optional className="ams-mb--sm">
Wat is uw e-mailadres?
{emailLabel}
</Label>
{emailDescription && (
<Paragraph size="small" id="email-input-description">
{emailDescription}
</Paragraph>
)}
<TextInput
aria-describedby={emailDescription ? 'email-input-description' : undefined}
name="email"
id="email-input"
type="email"
Expand All @@ -51,9 +64,21 @@ export const Contact = () => {
className="ams-mb--sm"
/>
<Label htmlFor="tel-input" optional className="ams-mb--sm">
Wat is uw telefoonnummer?
{telLabel}
</Label>
<TextInput name="phone" id="tel-input" type="tel" autoComplete="tel" className="ams-mb--sm" />
{telDescription && (
<Paragraph size="small" id="tel-input-description">
{telDescription}
</Paragraph>
)}
<TextInput
aria-describedby={telDescription ? 'tel-input-description' : undefined}
autoComplete="tel"
className="ams-mb--sm"
id="tel-input"
name="phone"
type="tel"
/>
<SubmitButton>Volgende vraag</SubmitButton>
</form>
</Grid.Cell>
Expand Down
23 changes: 23 additions & 0 deletions apps/public/src/app/(general)/contact/page.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { render, screen } from '@testing-library/react'
import { http, HttpResponse } from 'msw'

import { ENDPOINTS } from 'apps/public/src/mocks/endpoints'
import { server } from 'apps/public/src/mocks/node'

import Page from './page'

Expand All @@ -14,4 +18,23 @@ describe('Page', () => {

expect(screen.getByText('Contact Component')).toBeInTheDocument()
})

it('shows an error message if no contact form is found', async () => {
server.use(
http.get(ENDPOINTS.STATIC_FORM, () =>
HttpResponse.json([
{
id: '123',
type: 'not-contact',
},
]),
),
)

const PageComponent = await Page()

render(PageComponent)

expect(screen.getByText('Contact form id not found')).toBeInTheDocument()
})
})
29 changes: 28 additions & 1 deletion apps/public/src/app/(general)/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
import type { Metadata } from 'next'

import type { StaticFormOutput, StaticFormTextAreaComponentOutput } from 'apps/public/src/apiClientProxy'
import { getStaticForm, getStaticFormByStaticFormId } from 'apps/public/src/apiClientProxy'

import { Contact } from './Contact'

// TODO: Force dynamic rendering for now, because the api isn't accessible in the pipeline yet.
// We can remove this when the api is deployed.
export const dynamic = 'force-dynamic'

export const metadata: Metadata = {
title: 'Stap 3 van 4 - Gegevens - Gemeente Amsterdam',
}

export default async () => <Contact />
const isTextArea = (
component: StaticFormOutput['components'][number],
): component is StaticFormTextAreaComponentOutput => component.type === 'textarea'

export default async () => {
try {
const contactFormId = await getStaticForm().then((response) => response.find((form) => form.type === 'contact')?.id)

if (!contactFormId) throw new Error('Contact form id not found')

const contactForm = (await getStaticFormByStaticFormId({ staticFormId: contactFormId })).components
// A contact form is always an array of two text area components, but TypeScript doesn't know that
// We use a type guard here to make sure we're always working with the right type
const filteredContactForm = contactForm.filter(isTextArea)

if (!filteredContactForm[0].label || !filteredContactForm[1].label) throw new Error('Contact form labels not found')
return <Contact formData={filteredContactForm} />
} catch (error) {
return (error as Error).message
}
}
6 changes: 4 additions & 2 deletions apps/public/src/app/(general)/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('Page', () => {
expect(Home).toHaveBeenCalledWith({ formData: mockFormData.components[0].components }, {})
})

it('returns undefined if no primary form is found', async () => {
it('throws an error if no primary form is found', async () => {
server.use(
http.get(ENDPOINTS.STATIC_FORM, () =>
HttpResponse.json([
Expand All @@ -36,6 +36,8 @@ describe('Page', () => {

const PageComponent = await Page()

expect(PageComponent).toBeUndefined()
render(PageComponent)

expect(screen.getByText('Primary form id not found')).toBeInTheDocument()
})
})
25 changes: 18 additions & 7 deletions apps/public/src/app/(general)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getTranslations } from 'next-intl/server'

import type { StaticFormTextAreaComponentOutput, StaticFormOutput } from 'apps/public/src/apiClientProxy'
import { getStaticForm, getStaticFormByStaticFormId } from 'apps/public/src/apiClientProxy'

import { Home } from './Home'
Expand All @@ -16,12 +17,22 @@ export const generateMetadata = async () => {
}
}

export default async () => {
const primaryFormId = await getStaticForm().then((response) => response.find((form) => form.type === 'primary')?.id)

if (!primaryFormId) return undefined

const primaryForm = (await getStaticFormByStaticFormId({ staticFormId: primaryFormId }))?.components
const isTextArea = (
component: StaticFormOutput['components'][number],
): component is StaticFormTextAreaComponentOutput => component.type === 'textarea'

return <Home formData={primaryForm} />
export default async () => {
try {
const primaryFormId = await getStaticForm().then((response) => response.find((form) => form.type === 'primary')?.id)

if (!primaryFormId) throw new Error('Primary form id not found')

const primaryForm = (await getStaticFormByStaticFormId({ staticFormId: primaryFormId })).components
// A primary form is always an array with 1 text area component, but TypeScript doesn't know that
// We use a type guard here to make sure we're always working with the right type
const filteredPrimaryForm = primaryForm.filter(isTextArea)
return <Home formData={filteredPrimaryForm} />
} catch (error) {
return (error as Error).message
}
}
17 changes: 15 additions & 2 deletions apps/public/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { http, HttpResponse } from 'msw'

import { ENDPOINTS } from './endpoints'
import mockAdditionalQuestionsAnswerData from './mockAdditionalQuestionsAnswerData.json'
import mockContactFormData from './mockContactFormData.json'
import mockFormData from './mockFormData.json'
import mockMeldingData from './mockMeldingData.json'

Expand Down Expand Up @@ -67,13 +68,25 @@ export const handlers = [
http.get(ENDPOINTS.STATIC_FORM, () =>
HttpResponse.json([
{
id: '123',
id: '1',
type: 'primary',
},
{
id: '2',
type: 'contact',
},
]),
),

http.get(ENDPOINTS.STATIC_FORM_BY_STATIC_FORM_ID, () => HttpResponse.json(mockFormData.components[0])),
http.get(ENDPOINTS.STATIC_FORM_BY_STATIC_FORM_ID, ({ params }) => {
if (params.staticFormId === '1') {
return HttpResponse.json(mockFormData.components[0])
}
if (params.staticFormId === '2') {
return HttpResponse.json(mockContactFormData)
}
return undefined
}),

http.get(ENDPOINTS.MELDING_BY_ID, () => HttpResponse.json(mockMeldingData)),

Expand Down
28 changes: 28 additions & 0 deletions apps/public/src/mocks/mockContactFormData.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"components": [
{
"label": "First question",
"description": "",
"key": "textArea1",
"type": "textarea",
"input": true,
"autoExpand": false,
"showCharCount": false,
"position": 1,
"question": 1,
"values": null
},
{
"label": "Second question",
"description": "",
"key": "textArea2",
"type": "textarea",
"input": true,
"autoExpand": false,
"showCharCount": false,
"position": 2,
"question": 2,
"values": null
}
]
}