Skip to content
Draft
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
4 changes: 2 additions & 2 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ app.get('/api/interrogations/:id', (c) => {
id: c.req.param('id'),
questionnaireId: c.req.param('id'),
data: {
COLLECTED: data
COLLECTED: data,
},
stateData,
})
})

app.patch('/api/interrogations/:id', async (c) => {
const json = (await c.req.json()) as any
data = {...data, ...json.data}
data = { ...data, ...json.data }
stateData = json.stateData
return c.json({})
})
Expand Down
1 change: 0 additions & 1 deletion src/components/orchestrator/hooks/interrogation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export function computeUpdatedData(
return currentInterrogationData
}


/**
* Retrieve the full data, merging the changes into the current data
*/
Expand Down
150 changes: 150 additions & 0 deletions src/components/orchestrator/slotComponents/Loop.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import * as focusUtils from '../utils/focusLastRowInput'
import { Loop } from './Loop'

vi.mock('../utils/focusLastRowInput', () => ({
focusLastInput: vi.fn(),
}))

const mockFocusLastInput = vi.mocked(focusUtils.focusLastInput)

describe('Loop', () => {
const defaultProps = {
id: 'test-loop',
label: 'Test Loop',
children: <div>Loop content</div>,
canControlRows: true,
addRow: vi.fn(),
removeRow: vi.fn(),
executeExpression: vi.fn(),
}

beforeEach(() => {
vi.clearAllMocks()
})

it('should render label and children', () => {
render(<Loop {...defaultProps} />)

expect(screen.getByText('Test Loop')).toBeInTheDocument()

const label = screen.getByText('Test Loop')
expect(label).toHaveAttribute('id', 'label-test-loop')

expect(screen.getByText('Loop content')).toBeInTheDocument()
})

it('should render description when provided', () => {
render(<Loop {...defaultProps} description="Test description" />)

expect(screen.getByText('Test description')).toBeInTheDocument()
})

it('should render control buttons when canControlRows is true', () => {
render(<Loop {...defaultProps} />)

expect(
screen.getByRole('button', { name: 'Ajouter une ligne' }),
).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Supprimer la dernière ligne' }),
).toBeInTheDocument()
})

it('should not render control buttons when canControlRows is false', () => {
render(<Loop {...defaultProps} canControlRows={false} />)

expect(
screen.queryByRole('button', { name: 'Ajouter une ligne' }),
).not.toBeInTheDocument()
expect(
screen.queryByRole('button', { name: 'Supprimer la dernière ligne' }),
).not.toBeInTheDocument()
})

it('should disable add button when addRow is not provided', () => {
render(<Loop {...defaultProps} addRow={undefined} />)

const addButton = screen.getByRole('button', { name: 'Ajouter une ligne' })
expect(addButton).toBeDisabled()
})

it('should disable remove button when removeRow is not provided', () => {
render(<Loop {...defaultProps} removeRow={undefined} />)

const removeButton = screen.getByRole('button', {
name: 'Supprimer la dernière ligne',
})
expect(removeButton).toBeDisabled()
})

it('should render errors when provided', () => {
const errors = [
{
id: 'error1',
errorMessage: 'First error',
criticality: 'ERROR' as const,
},
{
id: 'error2',
errorMessage: 'Second error',
criticality: 'INFO' as const,
},
]

render(<Loop {...defaultProps} errors={errors} />)

expect(screen.getByRole('alert')).toBeInTheDocument()
expect(screen.getByText('First error')).toBeInTheDocument()
expect(screen.getByText('Second error')).toBeInTheDocument()
})

it('should not render error container when no errors', () => {
render(<Loop {...defaultProps} errors={[]} />)

expect(screen.queryByRole('alert')).not.toBeInTheDocument()
})

it('should call addRow and focus when add button is clicked', async () => {
render(<Loop {...defaultProps} />)

const addButton = screen.getByRole('button', { name: 'Ajouter une ligne' })
fireEvent.click(addButton)

expect(defaultProps.addRow).toHaveBeenCalled()

await waitFor(() => {
expect(mockFocusLastInput).toHaveBeenCalled()
})
})

it('should call removeRow and focus when remove button is clicked', async () => {
render(<Loop {...defaultProps} />)

const removeButton = screen.getByRole('button', {
name: 'Supprimer la dernière ligne',
})
fireEvent.click(removeButton)

expect(defaultProps.removeRow).toHaveBeenCalled()

await waitFor(() => {
expect(mockFocusLastInput).toHaveBeenCalled()
})
})

it('should call focusLastInput with correct container', async () => {
render(<Loop {...defaultProps} />)

const addButton = screen.getByRole('button', { name: 'Ajouter une ligne' })
fireEvent.click(addButton)

await waitFor(() => {
expect(mockFocusLastInput).toHaveBeenCalledWith(
expect.any(HTMLDivElement),
)
})
})
})
34 changes: 30 additions & 4 deletions src/components/orchestrator/slotComponents/Loop.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useRef } from 'react'

import { fr } from '@codegouvfr/react-dsfr'
import Alert from '@codegouvfr/react-dsfr/Alert'
import { ButtonsGroup } from '@codegouvfr/react-dsfr/ButtonsGroup'
import type { LunaticSlotComponents } from '@inseefr/lunatic'

import { focusLastInput } from '../utils/focusLastRowInput'

export const Loop: LunaticSlotComponents['Loop'] = (props) => {
const {
declarations,
Expand All @@ -15,6 +19,26 @@ export const Loop: LunaticSlotComponents['Loop'] = (props) => {
addRow,
removeRow,
} = props
const childrenRef = useRef<HTMLDivElement>(null)

const handleAddRow = () => {
addRow?.()
setTimeout(() => {
if (childrenRef.current) {
// Needed to bypass the focuskey being overwritten by react-dsfr
focusLastInput(childrenRef.current)
}
}, 0)
}

const handleRemoveRow = () => {
removeRow?.()
setTimeout(() => {
if (childrenRef.current) {
focusLastInput(childrenRef.current)
}
}, 0)
}

if (declarations) {
//TODO throw and handle globaly errors in an alert with a condition to avoid to display alert in prod
Expand All @@ -35,7 +59,7 @@ export const Loop: LunaticSlotComponents['Loop'] = (props) => {
if (!error.errorMessage) {
//TODO throw error
console.error(`The error : ${error.id} do not contains message`)
return
return null
}
return (
<Alert
Expand All @@ -50,21 +74,23 @@ export const Loop: LunaticSlotComponents['Loop'] = (props) => {
})}
</div>
)}
{children}
<div ref={childrenRef} tabIndex={-1}>
{children}
</div>
{canControlRows && (
<ButtonsGroup
alignment="left"
buttons={[
{
priority: 'secondary',
children: 'Ajouter une ligne',
onClick: addRow,
onClick: handleAddRow,
disabled: !addRow,
},
{
priority: 'tertiary',
children: 'Supprimer la dernière ligne',
onClick: removeRow,
onClick: handleRemoveRow,
disabled: !removeRow,
},
]}
Expand Down
127 changes: 127 additions & 0 deletions src/components/orchestrator/utils/focusLastRowInput.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { focusLastInput } from './focusLastRowInput'

describe('focusLastInput', () => {
let container: HTMLDivElement

beforeEach(() => {
container = document.createElement('div')
document.body.appendChild(container)
vi.clearAllMocks()
})

afterEach(() => {
document.body.removeChild(container)
})

describe('table structure', () => {
it('should focus on first input of last table row', () => {
container.innerHTML = `
<table>
<tbody>
<tr>
<td><input id="input1" /></td>
<td><input id="input2" /></td>
</tr>
<tr>
<td><input id="input3" /></td>
<td><input id="input4" /></td>
</tr>
</tbody>
</table>
`

const input3 = container.querySelector('#input3') as HTMLInputElement
const focusSpy = vi.spyOn(input3, 'focus')

focusLastInput(container)

expect(focusSpy).toHaveBeenCalled()
})

it('should handle table without tbody', () => {
container.innerHTML = `
<table>
<tr>
<td><input id="input1" /></td>
</tr>
<tr>
<td><input id="input2" /></td>
</tr>
</table>
`

const input2 = container.querySelector('#input2') as HTMLInputElement
const focusSpy = vi.spyOn(input2, 'focus')

focusLastInput(container)

expect(focusSpy).toHaveBeenCalled()
})

it('should handle empty table', () => {
container.innerHTML = `<table></table>`
const containerFocusSpy = vi.spyOn(container, 'focus')

focusLastInput(container)

expect(containerFocusSpy).toHaveBeenCalled()
})

it('should handle table row without inputs', () => {
container.innerHTML = `
<table>
<tr><td>No input here</td></tr>
</table>
`
const containerFocusSpy = vi.spyOn(container, 'focus')

focusLastInput(container)

expect(containerFocusSpy).toHaveBeenCalled()
})
})
it('should focus on last input when no table present', () => {
container.innerHTML = `
<div>
<input id="input1" />
<input id="input2" />
<input id="input3" />
</div>
`

const input3 = container.querySelector('#input3') as HTMLInputElement
const focusSpy = vi.spyOn(input3, 'focus')

focusLastInput(container)

expect(focusSpy).toHaveBeenCalled()
})

it('should focus on single input', () => {
container.innerHTML = `<input id="input1" />`

const input1 = container.querySelector('#input1') as HTMLInputElement
const focusSpy = vi.spyOn(input1, 'focus')

focusLastInput(container)

expect(focusSpy).toHaveBeenCalled()
})

it('should focus on last div when no inputs available', () => {
container.innerHTML = `
<div>First div</div>
<div id="lastDiv">Last div</div>
`

const lastDiv = container.querySelector('#lastDiv') as HTMLElement
const focusSpy = vi.spyOn(lastDiv, 'focus')

focusLastInput(container)

expect(lastDiv.getAttribute('tabindex')).toBe('-1')
expect(focusSpy).toHaveBeenCalled()
})
})
Loading
Loading