Files
gremiumhub/legalconsenthub/test/unit/formDiff.spec.ts

1075 lines
38 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { compareApplicationFormValues, groupChangesBySection } from '../../app/utils/formDiff'
import type {
ApplicationFormDto,
ApplicationFormSnapshotDto,
FormElementDto,
FormElementSnapshotDto,
FormOptionDto,
FormElementSectionDto,
FormElementSubSectionDto,
FormElementSectionSnapshotDto,
FormElementSubSectionSnapshotDto,
FormElementType
} from '../../.api-client'
// Helper to create a minimal FormOptionDto
function createOption(value: string, label: string): FormOptionDto {
return {
value,
label,
processingPurpose: 'NONE',
employeeDataCategory: 'NONE'
}
}
// Helper to create a FormElementDto
function createFormElement(
reference: string,
title: string,
type: FormElementType,
options: FormOptionDto[]
): FormElementDto {
return {
id: `id-${reference}`,
reference,
title,
type,
options
}
}
// Helper to create a FormElementSnapshotDto
function createSnapshotElement(
reference: string,
title: string,
type: FormElementType,
options: FormOptionDto[]
): FormElementSnapshotDto {
return {
reference,
title,
type,
options
}
}
// Helper to create an ApplicationFormDto with a single element
function createForm(elements: FormElementDto[], sectionTitle = 'Test Section'): ApplicationFormDto {
const subSection: FormElementSubSectionDto = {
id: 'subsection-1',
title: 'Subsection',
formElements: elements
}
const section: FormElementSectionDto = {
id: 'section-1',
title: sectionTitle,
formElementSubSections: [subSection]
}
return {
id: 'form-1',
name: 'Test Form',
isTemplate: false,
formElementSections: [section]
}
}
// Helper to create an ApplicationFormSnapshotDto with a single element
function createSnapshot(elements: FormElementSnapshotDto[], sectionTitle = 'Test Section'): ApplicationFormSnapshotDto {
const subSection: FormElementSubSectionSnapshotDto = {
title: 'Subsection',
elements
}
const section: FormElementSectionSnapshotDto = {
title: sectionTitle,
subsections: [subSection]
}
return {
name: 'Test Form',
status: 'DRAFT',
organizationId: 'org-1',
sections: [section]
}
}
describe('formDiff', () => {
describe('compareApplicationFormValues', () => {
describe('TEXTFIELD element', () => {
it('should detect new answer (empty → filled)', () => {
const current = createForm([
createFormElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('John Doe', '')])
])
const version = createSnapshot([
createSnapshotElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].elementTitle).toBe('Name')
expect(diff.newAnswers[0].currentLabel).toBe('John Doe')
expect(diff.newAnswers[0].previousLabel).toBeNull()
expect(diff.changedAnswers).toHaveLength(0)
expect(diff.clearedAnswers).toHaveLength(0)
})
it('should detect changed answer', () => {
const current = createForm([
createFormElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('Jane Doe', '')])
])
const version = createSnapshot([
createSnapshotElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('John Doe', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('John Doe')
expect(diff.changedAnswers[0].currentLabel).toBe('Jane Doe')
expect(diff.newAnswers).toHaveLength(0)
expect(diff.clearedAnswers).toHaveLength(0)
})
it('should detect cleared answer (filled → empty)', () => {
const current = createForm([createFormElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('', '')])])
const version = createSnapshot([
createSnapshotElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('John Doe', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('John Doe')
expect(diff.clearedAnswers[0].currentLabel).toBeNull()
expect(diff.newAnswers).toHaveLength(0)
expect(diff.changedAnswers).toHaveLength(0)
})
it('should ignore unchanged values', () => {
const current = createForm([
createFormElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('John Doe', '')])
])
const version = createSnapshot([
createSnapshotElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('John Doe', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(0)
expect(diff.changedAnswers).toHaveLength(0)
expect(diff.clearedAnswers).toHaveLength(0)
})
})
describe('TEXTAREA element', () => {
it('should detect new answer', () => {
const current = createForm([
createFormElement('textarea_1', 'Description', 'TEXTAREA', [
createOption('This is a long description text.', '')
])
])
const version = createSnapshot([
createSnapshotElement('textarea_1', 'Description', 'TEXTAREA', [createOption('', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('This is a long description text.')
})
it('should detect changed answer', () => {
const current = createForm([
createFormElement('textarea_1', 'Description', 'TEXTAREA', [createOption('Updated text', '')])
])
const version = createSnapshot([
createSnapshotElement('textarea_1', 'Description', 'TEXTAREA', [createOption('Original text', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Original text')
expect(diff.changedAnswers[0].currentLabel).toBe('Updated text')
})
it('should detect cleared answer', () => {
const current = createForm([createFormElement('textarea_1', 'Description', 'TEXTAREA', [createOption('', '')])])
const version = createSnapshot([
createSnapshotElement('textarea_1', 'Description', 'TEXTAREA', [createOption('Some text', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Some text')
})
})
describe('RICH_TEXT element', () => {
it('should detect new answer', () => {
const current = createForm([
createFormElement('richtext_1', 'Notes', 'RICH_TEXT', [
createOption('<p>Rich <strong>text</strong> content</p>', '')
])
])
const version = createSnapshot([
createSnapshotElement('richtext_1', 'Notes', 'RICH_TEXT', [createOption('', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('<p>Rich <strong>text</strong> content</p>')
})
it('should detect changed answer', () => {
const current = createForm([
createFormElement('richtext_1', 'Notes', 'RICH_TEXT', [createOption('<p>Updated</p>', '')])
])
const version = createSnapshot([
createSnapshotElement('richtext_1', 'Notes', 'RICH_TEXT', [createOption('<p>Original</p>', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('<p>Original</p>')
expect(diff.changedAnswers[0].currentLabel).toBe('<p>Updated</p>')
})
it('should detect cleared answer', () => {
const current = createForm([createFormElement('richtext_1', 'Notes', 'RICH_TEXT', [createOption('', '')])])
const version = createSnapshot([
createSnapshotElement('richtext_1', 'Notes', 'RICH_TEXT', [createOption('<p>Content</p>', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
})
})
describe('DATE element', () => {
it('should detect new answer', () => {
const current = createForm([
createFormElement('date_1', 'Start Date', 'DATE', [createOption('2024-01-15', '')])
])
const version = createSnapshot([createSnapshotElement('date_1', 'Start Date', 'DATE', [createOption('', '')])])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('2024-01-15')
})
it('should detect changed answer', () => {
const current = createForm([
createFormElement('date_1', 'Start Date', 'DATE', [createOption('2024-02-20', '')])
])
const version = createSnapshot([
createSnapshotElement('date_1', 'Start Date', 'DATE', [createOption('2024-01-15', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('2024-01-15')
expect(diff.changedAnswers[0].currentLabel).toBe('2024-02-20')
})
it('should detect cleared answer', () => {
const current = createForm([createFormElement('date_1', 'Start Date', 'DATE', [createOption('', '')])])
const version = createSnapshot([
createSnapshotElement('date_1', 'Start Date', 'DATE', [createOption('2024-01-15', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
})
})
describe('SELECT element', () => {
it('should detect new answer (nothing selected → option selected)', () => {
const current = createForm([
createFormElement('select_1', 'Priority', 'SELECT', [
createOption('false', 'Low'),
createOption('true', 'Medium'),
createOption('false', 'High')
])
])
const version = createSnapshot([
createSnapshotElement('select_1', 'Priority', 'SELECT', [
createOption('false', 'Low'),
createOption('false', 'Medium'),
createOption('false', 'High')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Medium')
expect(diff.newAnswers[0].previousLabel).toBeNull()
})
it('should detect changed answer (different option selected)', () => {
const current = createForm([
createFormElement('select_1', 'Priority', 'SELECT', [
createOption('false', 'Low'),
createOption('false', 'Medium'),
createOption('true', 'High')
])
])
const version = createSnapshot([
createSnapshotElement('select_1', 'Priority', 'SELECT', [
createOption('true', 'Low'),
createOption('false', 'Medium'),
createOption('false', 'High')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Low')
expect(diff.changedAnswers[0].currentLabel).toBe('High')
})
it('should detect cleared answer (option selected → nothing selected)', () => {
const current = createForm([
createFormElement('select_1', 'Priority', 'SELECT', [
createOption('false', 'Low'),
createOption('false', 'Medium'),
createOption('false', 'High')
])
])
const version = createSnapshot([
createSnapshotElement('select_1', 'Priority', 'SELECT', [
createOption('false', 'Low'),
createOption('true', 'Medium'),
createOption('false', 'High')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Medium')
expect(diff.clearedAnswers[0].currentLabel).toBeNull()
})
})
describe('RADIOBUTTON element', () => {
it('should detect new answer', () => {
const current = createForm([
createFormElement('radio_1', 'Gender', 'RADIOBUTTON', [
createOption('true', 'Male'),
createOption('false', 'Female'),
createOption('false', 'Other')
])
])
const version = createSnapshot([
createSnapshotElement('radio_1', 'Gender', 'RADIOBUTTON', [
createOption('false', 'Male'),
createOption('false', 'Female'),
createOption('false', 'Other')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Male')
})
it('should detect changed answer', () => {
const current = createForm([
createFormElement('radio_1', 'Gender', 'RADIOBUTTON', [
createOption('false', 'Male'),
createOption('true', 'Female'),
createOption('false', 'Other')
])
])
const version = createSnapshot([
createSnapshotElement('radio_1', 'Gender', 'RADIOBUTTON', [
createOption('true', 'Male'),
createOption('false', 'Female'),
createOption('false', 'Other')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Male')
expect(diff.changedAnswers[0].currentLabel).toBe('Female')
})
it('should detect cleared answer', () => {
const current = createForm([
createFormElement('radio_1', 'Gender', 'RADIOBUTTON', [
createOption('false', 'Male'),
createOption('false', 'Female'),
createOption('false', 'Other')
])
])
const version = createSnapshot([
createSnapshotElement('radio_1', 'Gender', 'RADIOBUTTON', [
createOption('false', 'Male'),
createOption('false', 'Female'),
createOption('true', 'Other')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Other')
})
})
describe('CHECKBOX element', () => {
it('should detect new answer (single checkbox selected)', () => {
const current = createForm([
createFormElement('checkbox_1', 'Features', 'CHECKBOX', [
createOption('true', 'Feature A'),
createOption('false', 'Feature B'),
createOption('false', 'Feature C')
])
])
const version = createSnapshot([
createSnapshotElement('checkbox_1', 'Features', 'CHECKBOX', [
createOption('false', 'Feature A'),
createOption('false', 'Feature B'),
createOption('false', 'Feature C')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Feature A')
})
it('should detect new answer (multiple checkboxes selected)', () => {
const current = createForm([
createFormElement('checkbox_1', 'Features', 'CHECKBOX', [
createOption('true', 'Feature A'),
createOption('true', 'Feature B'),
createOption('false', 'Feature C')
])
])
const version = createSnapshot([
createSnapshotElement('checkbox_1', 'Features', 'CHECKBOX', [
createOption('false', 'Feature A'),
createOption('false', 'Feature B'),
createOption('false', 'Feature C')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Feature A, Feature B')
})
it('should detect changed answer (different checkboxes selected)', () => {
const current = createForm([
createFormElement('checkbox_1', 'Features', 'CHECKBOX', [
createOption('false', 'Feature A'),
createOption('true', 'Feature B'),
createOption('true', 'Feature C')
])
])
const version = createSnapshot([
createSnapshotElement('checkbox_1', 'Features', 'CHECKBOX', [
createOption('true', 'Feature A'),
createOption('false', 'Feature B'),
createOption('false', 'Feature C')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].previousLabel).toBe('Feature A')
expect(diff.changedAnswers[0].currentLabel).toBe('Feature B, Feature C')
})
it('should detect cleared answer (all checkboxes deselected)', () => {
const current = createForm([
createFormElement('checkbox_1', 'Features', 'CHECKBOX', [
createOption('false', 'Feature A'),
createOption('false', 'Feature B'),
createOption('false', 'Feature C')
])
])
const version = createSnapshot([
createSnapshotElement('checkbox_1', 'Features', 'CHECKBOX', [
createOption('true', 'Feature A'),
createOption('true', 'Feature B'),
createOption('false', 'Feature C')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Feature A, Feature B')
})
})
describe('SWITCH element', () => {
it('should detect new answer (switch turned on)', () => {
const current = createForm([
createFormElement('switch_1', 'Enable Notifications', 'SWITCH', [createOption('true', 'Enabled')])
])
const version = createSnapshot([
createSnapshotElement('switch_1', 'Enable Notifications', 'SWITCH', [createOption('false', 'Enabled')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('Enabled')
})
it('should detect cleared answer (switch turned off)', () => {
const current = createForm([
createFormElement('switch_1', 'Enable Notifications', 'SWITCH', [createOption('false', 'Enabled')])
])
const version = createSnapshot([
createSnapshotElement('switch_1', 'Enable Notifications', 'SWITCH', [createOption('true', 'Enabled')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('Enabled')
})
})
describe('TABLE element', () => {
it('should detect new rows added', () => {
const current = createForm([
createFormElement('table_1', 'Employees', 'TABLE', [
createOption('["John", "Jane"]', 'Name'),
createOption('["Developer", "Designer"]', 'Role')
])
])
const version = createSnapshot([
createSnapshotElement('table_1', 'Employees', 'TABLE', [
createOption('[]', 'Name'),
createOption('[]', 'Role')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('2 Zeilen')
expect(diff.newAnswers[0].tableDiff).toBeDefined()
expect(diff.newAnswers[0].tableDiff!.addedCount).toBe(2)
expect(diff.newAnswers[0].tableDiff!.removedCount).toBe(0)
expect(diff.newAnswers[0].tableDiff!.modifiedCount).toBe(0)
})
it('should detect rows removed', () => {
const current = createForm([
createFormElement('table_1', 'Employees', 'TABLE', [createOption('[]', 'Name'), createOption('[]', 'Role')])
])
const version = createSnapshot([
createSnapshotElement('table_1', 'Employees', 'TABLE', [
createOption('["John", "Jane"]', 'Name'),
createOption('["Developer", "Designer"]', 'Role')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].previousLabel).toBe('2 Zeilen')
expect(diff.clearedAnswers[0].tableDiff).toBeDefined()
expect(diff.clearedAnswers[0].tableDiff!.removedCount).toBe(2)
expect(diff.clearedAnswers[0].tableDiff!.addedCount).toBe(0)
})
it('should detect modified rows when row count changes', () => {
// Note: The diff algorithm compares by label first ("N Zeilen").
// If row count is the same, changes are detected. If row count differs, it's a change.
const current = createForm([
createFormElement('table_1', 'Employees', 'TABLE', [
createOption('["John", "Janet", "Bob"]', 'Name'),
createOption('["Developer", "Designer", "Tester"]', 'Role')
])
])
const version = createSnapshot([
createSnapshotElement('table_1', 'Employees', 'TABLE', [
createOption('["John", "Jane"]', 'Name'),
createOption('["Developer", "Designer"]', 'Role')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff).toBeDefined()
expect(diff.changedAnswers[0].tableDiff!.addedCount).toBe(1)
expect(diff.changedAnswers[0].tableDiff!.modifiedCount).toBe(1)
expect(diff.changedAnswers[0].tableDiff!.rows[1].changeType).toBe('modified')
expect(diff.changedAnswers[0].tableDiff!.rows[1].previousValues['Name']).toBe('Jane')
expect(diff.changedAnswers[0].tableDiff!.rows[1].currentValues['Name']).toBe('Janet')
})
it('should detect mixed changes (added, removed, modified)', () => {
const current = createForm([
createFormElement('table_1', 'Employees', 'TABLE', [
createOption('["John Updated", "New Person"]', 'Name'),
createOption('["Senior Dev", "Manager"]', 'Role')
])
])
const version = createSnapshot([
createSnapshotElement('table_1', 'Employees', 'TABLE', [
createOption('["John", "Jane", "Bob"]', 'Name'),
createOption('["Developer", "Designer", "Tester"]', 'Role')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff).toBeDefined()
// Row 0: modified (John → John Updated, Developer → Senior Dev)
// Row 1: modified (Jane → New Person, Designer → Manager)
// Row 2: removed (Bob, Tester)
expect(diff.changedAnswers[0].tableDiff!.modifiedCount).toBe(2)
expect(diff.changedAnswers[0].tableDiff!.removedCount).toBe(1)
})
it('should handle single row correctly', () => {
const current = createForm([
createFormElement('table_1', 'Employees', 'TABLE', [
createOption('["John"]', 'Name'),
createOption('["Developer"]', 'Role')
])
])
const version = createSnapshot([
createSnapshotElement('table_1', 'Employees', 'TABLE', [
createOption('[]', 'Name'),
createOption('[]', 'Role')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].currentLabel).toBe('1 Zeile')
})
it('should handle boolean values in table cells', () => {
// Note: The diff algorithm compares by label first ("N Zeilen").
// If row count is the same, no change is detected at the label level.
// To detect boolean cell changes, we need a row count change too.
const current = createForm([
createFormElement('table_1', 'Settings', 'TABLE', [
createOption('["Feature A", "Feature B"]', 'Name'),
createOption('[true, false]', 'Enabled')
])
])
const version = createSnapshot([
createSnapshotElement('table_1', 'Settings', 'TABLE', [
createOption('["Feature A"]', 'Name'),
createOption('[false]', 'Enabled')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff!.rows[0].previousValues['Enabled']).toBe('Nein')
expect(diff.changedAnswers[0].tableDiff!.rows[0].currentValues['Enabled']).toBe('Ja')
})
it('should handle array values in table cells', () => {
// Note: The diff algorithm compares by label first ("N Zeilen").
// To detect array cell changes, we need a row count change too.
const current = createForm([
createFormElement('table_1', 'Projects', 'TABLE', [
createOption('["Project A", "Project B"]', 'Name'),
createOption('[["Tag1", "Tag2"], ["Tag3"]]', 'Tags')
])
])
const version = createSnapshot([
createSnapshotElement('table_1', 'Projects', 'TABLE', [
createOption('["Project A"]', 'Name'),
createOption('[["Tag1"]]', 'Tags')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].tableDiff!.rows[0].previousValues['Tags']).toBe('Tag1')
expect(diff.changedAnswers[0].tableDiff!.rows[0].currentValues['Tags']).toBe('Tag1, Tag2')
})
})
describe('element removed from form', () => {
it('should detect when element is removed and had a value', () => {
const current = createForm([])
const version = createSnapshot([
createSnapshotElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('John Doe', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].elementTitle).toBe('Name')
expect(diff.clearedAnswers[0].previousLabel).toBe('John Doe')
})
it('should not report removed element if it had no value', () => {
const current = createForm([])
const version = createSnapshot([
createSnapshotElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('', '')])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.clearedAnswers).toHaveLength(0)
})
})
describe('element added to form', () => {
it('should detect new element with value as new answer', () => {
const current = createForm([
createFormElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('John Doe', '')])
])
const version = createSnapshot([])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].elementTitle).toBe('Name')
expect(diff.newAnswers[0].currentLabel).toBe('John Doe')
})
it('should not report new element if it has no value', () => {
const current = createForm([createFormElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('', '')])])
const version = createSnapshot([])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(0)
})
})
describe('multiple elements', () => {
it('should handle multiple elements with different change types', () => {
const current = createForm([
createFormElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('John Doe', '')]),
createFormElement('textfield_2', 'Email', 'TEXTFIELD', [createOption('new@email.com', '')]),
createFormElement('select_1', 'Status', 'SELECT', [
createOption('false', 'Active'),
createOption('false', 'Inactive')
])
])
const version = createSnapshot([
createSnapshotElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('', '')]),
createSnapshotElement('textfield_2', 'Email', 'TEXTFIELD', [createOption('old@email.com', '')]),
createSnapshotElement('select_1', 'Status', 'SELECT', [
createOption('true', 'Active'),
createOption('false', 'Inactive')
])
])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(1)
expect(diff.newAnswers[0].elementTitle).toBe('Name')
expect(diff.changedAnswers).toHaveLength(1)
expect(diff.changedAnswers[0].elementTitle).toBe('Email')
expect(diff.clearedAnswers).toHaveLength(1)
expect(diff.clearedAnswers[0].elementTitle).toBe('Status')
})
})
describe('multiple sections', () => {
it('should track section titles correctly', () => {
const section1: FormElementSectionDto = {
id: 'section-1',
title: 'Personal Info',
formElementSubSections: [
{
id: 'sub-1',
title: 'Basic',
formElements: [createFormElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('John', '')])]
}
]
}
const section2: FormElementSectionDto = {
id: 'section-2',
title: 'Contact Info',
formElementSubSections: [
{
id: 'sub-2',
title: 'Email',
formElements: [
createFormElement('textfield_2', 'Email', 'TEXTFIELD', [createOption('john@example.com', '')])
]
}
]
}
const current: ApplicationFormDto = {
id: 'form-1',
name: 'Test Form',
isTemplate: false,
formElementSections: [section1, section2]
}
const versionSection1: FormElementSectionSnapshotDto = {
title: 'Personal Info',
subsections: [
{
title: 'Basic',
elements: [createSnapshotElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('', '')])]
}
]
}
const versionSection2: FormElementSectionSnapshotDto = {
title: 'Contact Info',
subsections: [
{
title: 'Email',
elements: [createSnapshotElement('textfield_2', 'Email', 'TEXTFIELD', [createOption('', '')])]
}
]
}
const version: ApplicationFormSnapshotDto = {
name: 'Test Form',
status: 'DRAFT',
organizationId: 'org-1',
sections: [versionSection1, versionSection2]
}
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(2)
expect(diff.newAnswers.find((c) => c.sectionTitle === 'Personal Info')).toBeDefined()
expect(diff.newAnswers.find((c) => c.sectionTitle === 'Contact Info')).toBeDefined()
})
})
describe('edge cases', () => {
it('should handle empty forms', () => {
const current = createForm([])
const version = createSnapshot([])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(0)
expect(diff.changedAnswers).toHaveLength(0)
expect(diff.clearedAnswers).toHaveLength(0)
})
it('should handle elements without reference', () => {
const elementWithoutRef: FormElementDto = {
id: 'id-1',
title: 'No Reference',
type: 'TEXTFIELD',
options: [createOption('value', '')]
}
const current = createForm([elementWithoutRef])
const version = createSnapshot([])
const diff = compareApplicationFormValues(current, version)
// Element without reference should be ignored
expect(diff.newAnswers).toHaveLength(0)
})
it('should handle whitespace-only text values as empty', () => {
const current = createForm([createFormElement('textfield_1', 'Name', 'TEXTFIELD', [createOption(' ', '')])])
const version = createSnapshot([
createSnapshotElement('textfield_1', 'Name', 'TEXTFIELD', [createOption('', '')])
])
const diff = compareApplicationFormValues(current, version)
// Whitespace-only should be treated as empty
expect(diff.newAnswers).toHaveLength(0)
expect(diff.changedAnswers).toHaveLength(0)
expect(diff.clearedAnswers).toHaveLength(0)
})
it('should handle null/undefined options gracefully', () => {
const current = createForm([createFormElement('textfield_1', 'Name', 'TEXTFIELD', [])])
const version = createSnapshot([createSnapshotElement('textfield_1', 'Name', 'TEXTFIELD', [])])
const diff = compareApplicationFormValues(current, version)
expect(diff.newAnswers).toHaveLength(0)
expect(diff.changedAnswers).toHaveLength(0)
expect(diff.clearedAnswers).toHaveLength(0)
})
})
})
describe('groupChangesBySection', () => {
it('should group changes by section title', () => {
const current: ApplicationFormDto = {
id: 'form-1',
name: 'Test Form',
isTemplate: false,
formElementSections: [
{
id: 'section-1',
title: 'Section A',
formElementSubSections: [
{
id: 'sub-1',
title: 'Sub',
formElements: [
createFormElement('text_1', 'Field 1', 'TEXTFIELD', [createOption('Value 1', '')]),
createFormElement('text_2', 'Field 2', 'TEXTFIELD', [createOption('Value 2', '')])
]
}
]
},
{
id: 'section-2',
title: 'Section B',
formElementSubSections: [
{
id: 'sub-2',
title: 'Sub',
formElements: [createFormElement('text_3', 'Field 3', 'TEXTFIELD', [createOption('Value 3', '')])]
}
]
}
]
}
const version: ApplicationFormSnapshotDto = {
name: 'Test Form',
status: 'DRAFT',
organizationId: 'org-1',
sections: [
{
title: 'Section A',
subsections: [
{
title: 'Sub',
elements: [
createSnapshotElement('text_1', 'Field 1', 'TEXTFIELD', [createOption('', '')]),
createSnapshotElement('text_2', 'Field 2', 'TEXTFIELD', [createOption('', '')])
]
}
]
},
{
title: 'Section B',
subsections: [
{
title: 'Sub',
elements: [createSnapshotElement('text_3', 'Field 3', 'TEXTFIELD', [createOption('', '')])]
}
]
}
]
}
const diff = compareApplicationFormValues(current, version)
const grouped = groupChangesBySection(diff)
expect(grouped).toHaveLength(2)
const sectionA = grouped.find((g) => g.sectionTitle === 'Section A')
expect(sectionA).toBeDefined()
expect(sectionA!.changes).toHaveLength(2)
const sectionB = grouped.find((g) => g.sectionTitle === 'Section B')
expect(sectionB).toBeDefined()
expect(sectionB!.changes).toHaveLength(1)
})
it('should return empty array when no changes', () => {
const diff = {
newAnswers: [],
changedAnswers: [],
clearedAnswers: []
}
const grouped = groupChangesBySection(diff)
expect(grouped).toHaveLength(0)
})
it('should combine all change types in the same section', () => {
const current: ApplicationFormDto = {
id: 'form-1',
name: 'Test Form',
isTemplate: false,
formElementSections: [
{
id: 'section-1',
title: 'Section A',
formElementSubSections: [
{
id: 'sub-1',
title: 'Sub',
formElements: [
createFormElement('text_1', 'New Field', 'TEXTFIELD', [createOption('New Value', '')]),
createFormElement('text_2', 'Changed Field', 'TEXTFIELD', [createOption('Updated', '')]),
createFormElement('text_3', 'Cleared Field', 'TEXTFIELD', [createOption('', '')])
]
}
]
}
]
}
const version: ApplicationFormSnapshotDto = {
name: 'Test Form',
status: 'DRAFT',
organizationId: 'org-1',
sections: [
{
title: 'Section A',
subsections: [
{
title: 'Sub',
elements: [
createSnapshotElement('text_1', 'New Field', 'TEXTFIELD', [createOption('', '')]),
createSnapshotElement('text_2', 'Changed Field', 'TEXTFIELD', [createOption('Original', '')]),
createSnapshotElement('text_3', 'Cleared Field', 'TEXTFIELD', [createOption('Was Here', '')])
]
}
]
}
]
}
const diff = compareApplicationFormValues(current, version)
const grouped = groupChangesBySection(diff)
expect(grouped).toHaveLength(1)
expect(grouped[0].sectionTitle).toBe('Section A')
expect(grouped[0].changes).toHaveLength(3)
})
})
})