import { describe, it, expect } from 'vitest' import { compareApplicationFormValues, groupChangesBySection } from '../../../app/utils/formSnapshotComparison' 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('formSnapshotComparison', () => { 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('

Rich text content

', '') ]) ]) 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('

Rich text content

') }) it('should detect changed answer', () => { const current = createForm([ createFormElement('richtext_1', 'Notes', 'RICH_TEXT', [createOption('

Updated

', '')]) ]) const version = createSnapshot([ createSnapshotElement('richtext_1', 'Notes', 'RICH_TEXT', [createOption('

Original

', '')]) ]) const diff = compareApplicationFormValues(current, version) expect(diff.changedAnswers).toHaveLength(1) expect(diff.changedAnswers[0]?.previousLabel).toBe('

Original

') expect(diff.changedAnswers[0]?.currentLabel).toBe('

Updated

') }) 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('

Content

', '')]) ]) 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 modified rows when row count is the same', () => { const current = createForm([ createFormElement('table_1', 'Employees', 'TABLE', [ createOption('["John", "Janet"]', 'Name'), createOption('["Developer", "Designer"]', '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(0) 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', () => { // 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', () => { 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) }) }) })