/** * External dependencies */ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; /** * Internal dependencies */ import Dataform from '../index'; const noop = () => {}; const fields = [ { id: 'title', label: 'Title', type: 'text' as const, }, { id: 'order', label: 'Order', type: 'integer' as const, }, { id: 'author', label: 'Author', type: 'integer' as const, elements: [ { value: 1, label: 'Jane' }, { value: 2, label: 'John' }, ], }, ]; const form = { fields: [ 'title', 'order', 'author' ], }; const data = { title: 'Hello World', author: 1, order: 1, }; const fieldsSelector = { title: { view: () => screen.getByRole( 'button', { name: /edit title/i, } ), edit: () => screen.getByRole( 'textbox', { name: /title/i, } ), }, author: { view: () => screen.getByRole( 'button', { name: /edit author/i, } ), edit: () => screen.queryByRole( 'combobox', { name: /author/i, } ), }, order: { view: () => screen.getByRole( 'button', { name: /edit order/i, } ), edit: () => screen.getByRole( 'spinbutton', { name: /order/i, } ), }, }; describe( 'DataForm component', () => { describe( 'in regular mode', () => { it( 'should display fields', () => { render( ); expect( fieldsSelector.title.edit() ).toBeInTheDocument(); expect( fieldsSelector.order.edit() ).toBeInTheDocument(); expect( fieldsSelector.author.edit() ).toBeInTheDocument(); } ); it( 'should render custom Edit component', () => { const fieldsWithCustomEditComponent = fields.map( ( field ) => { if ( field.id === 'title' ) { return { ...field, Edit: () => { return This is the Title Field; }, }; } return field; } ); render( ); const titleField = screen.getByText( 'This is the Title Field' ); expect( titleField ).toBeInTheDocument(); } ); it( 'should call onChange with the correct value for each typed character', async () => { const onChange = jest.fn(); render( ); const titleInput = fieldsSelector.title.edit(); const user = userEvent.setup(); await user.clear( titleInput ); expect( titleInput ).toHaveValue( '' ); const newValue = 'Hello folks!'; await user.type( titleInput, newValue ); expect( onChange ).toHaveBeenCalledTimes( newValue.length ); for ( let i = 0; i < newValue.length; i++ ) { expect( onChange ).toHaveBeenNthCalledWith( i + 1, { title: newValue.slice( 0, i + 1 ), } ); } } ); it( 'should allow decimal input for number fields', async () => { const onChange = jest.fn(); const fieldsWithNumber = [ ...fields, { id: 'price', label: 'Price', type: 'number' as const, }, ]; const formWithNumber = { ...form, fields: [ ...form.fields, 'price' ], }; render( ); const priceInput = screen.getByRole( 'spinbutton', { name: /price/i, } ); expect( priceInput ).toHaveValue( 2.5 ); const user = userEvent.setup(); await user.clear( priceInput ); await user.type( priceInput, '3.75' ); expect( onChange ).toHaveBeenLastCalledWith( { price: 3.75 } ); expect( priceInput ).toHaveValue( 3.75 ); } ); it( 'should render combined fields correctly', async () => { const formWithCombinedFields = { fields: [ 'order', { id: 'title', children: [ 'title', 'author' ], label: "Title and author's name", }, ], }; render( ); expect( screen.getByText( "Title and author's name" ) ).toBeInTheDocument(); } ); } ); describe( 'in panel mode', () => { const formPanelMode = { ...form, layout: { type: 'panel', labelPosition: 'side', } as const, }; it( 'should display fields', async () => { render( ); const user = await userEvent.setup(); for ( const field of Object.values( fieldsSelector ) ) { const button = field.view(); await user.click( button ); expect( field.edit() ).toBeInTheDocument(); } } ); it( 'should use dropdown panel type by default', async () => { render( ); const user = await userEvent.setup(); const titleButton = fieldsSelector.title.view(); await user.click( titleButton ); // Should show dropdown content (not modal) expect( screen.getByRole( 'textbox', { name: /title/i } ) ).toBeInTheDocument(); // Should not have modal dialog expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument(); // Should not have modal buttons (Cancel/Apply) expect( screen.queryByRole( 'button', { name: /cancel/i } ) ).not.toBeInTheDocument(); expect( screen.queryByRole( 'button', { name: /apply/i } ) ).not.toBeInTheDocument(); } ); it( 'should use dropdown panel type when explicitly set', async () => { const formWithDropdownPanel = { ...form, layout: { type: 'panel', labelPosition: 'side', openAs: 'dropdown', } as const, }; render( ); const user = await userEvent.setup(); const titleButton = fieldsSelector.title.view(); await user.click( titleButton ); // Should show dropdown content expect( screen.getByRole( 'textbox', { name: /title/i } ) ).toBeInTheDocument(); } ); it( 'should use modal panel type when set', async () => { const formWithModalPanel = { ...form, layout: { type: 'panel', labelPosition: 'side', openAs: 'modal', } as const, }; render( ); const user = await userEvent.setup(); const titleButton = fieldsSelector.title.view(); await user.click( titleButton ); // Should show modal content expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument(); expect( screen.getByRole( 'textbox', { name: /title/i } ) ).toBeInTheDocument(); } ); it( 'should close modal when cancel button is clicked', async () => { const formWithModalPanel = { ...form, layout: { type: 'panel', labelPosition: 'side', openAs: 'modal', } as const, }; render( ); const user = await userEvent.setup(); const titleButton = fieldsSelector.title.view(); await user.click( titleButton ); // Modal should be open expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument(); // Click cancel button const cancelButton = screen.getByRole( 'button', { name: /cancel/i, } ); await user.click( cancelButton ); // Modal should be closed expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument(); } ); it( 'should apply changes and close modal when apply button is clicked', async () => { const onChange = jest.fn(); const formWithModalPanel = { ...form, layout: { type: 'panel', labelPosition: 'side', openAs: 'modal', } as const, }; render( ); const user = await userEvent.setup(); const titleButton = fieldsSelector.title.view(); await user.click( titleButton ); // Modal should be open expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument(); // Type in the input const titleInput = screen.getByRole( 'textbox', { name: /title/i, } ); await user.clear( titleInput ); await user.type( titleInput, 'New Title' ); // Click apply button const applyButton = screen.getByRole( 'button', { name: /apply/i, } ); await user.click( applyButton ); // Modal should be closed and onChange should be called expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument(); expect( onChange ).toHaveBeenCalledWith( { title: 'New Title' } ); } ); it( 'should call onChange with the correct value for each typed character', async () => { const onChange = jest.fn(); render( ); const titleButton = fieldsSelector.title.view(); const user = await userEvent.setup(); await user.click( titleButton ); const input = fieldsSelector.title.edit(); expect( input ).toHaveValue( '' ); const newValue = 'Hello folks!'; await user.type( input, newValue ); expect( onChange ).toHaveBeenCalledTimes( newValue.length ); for ( let i = 0; i < newValue.length; i++ ) { expect( onChange ).toHaveBeenNthCalledWith( i + 1, { title: newValue.slice( 0, i + 1 ), } ); } } ); it( 'should render combined fields correctly', async () => { const formWithCombinedFields = { ...formPanelMode, fields: [ 'order', { id: 'title', children: [ 'title', 'author' ], label: "Title and author's name", }, ], }; render( ); const button = screen.getByRole( 'button', { name: /edit title and author's name/i, } ); const user = await userEvent.setup(); await user.click( button ); expect( fieldsSelector.title.edit() ).toBeInTheDocument(); expect( fieldsSelector.author.edit() ).toBeInTheDocument(); } ); it( 'should render custom render component', async () => { const fieldsWithCustomRenderFunction = fields.map( ( field ) => { return { ...field, render: () => { return This is the { field.id } field; }, }; } ); render( ); const titleField = screen.getByText( 'This is the title field' ); const orderField = screen.getByText( 'This is the order field' ); const authorField = screen.getByText( 'This is the author field' ); expect( titleField ).toBeInTheDocument(); expect( orderField ).toBeInTheDocument(); expect( authorField ).toBeInTheDocument(); } ); it( 'should render custom Edit component', async () => { const fieldsWithTitleCustomEditComponent = fields.map( ( field ) => { if ( field.id === 'title' ) { return { ...field, Edit: () => { return This is the Title Field; }, }; } return field; } ); render( ); const titleButton = fieldsSelector.title.view(); const user = await userEvent.setup(); await user.click( titleButton ); const titleEditField = screen.getByText( 'This is the Title Field' ); expect( titleEditField ).toBeInTheDocument(); } ); } ); } );