/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { vi } from 'vitest'; import { FlowEngine } from '../../flowEngine'; import { FlowModel } from '../../models/flowModel'; import { FlowEngineProvider } from '../../provider'; import { FieldModelRenderer } from '../FieldModelRenderer'; class InputModel extends FlowModel { render(): React.ReactNode { return ( ); } } describe('FieldModelRenderer', () => { it('updates model and form value for normal input changes', async () => { const flowEngine = new FlowEngine(); const model = new InputModel({ uid: 'normal-input-field-model', flowEngine, props: { value: '', }, }); model.dispatchEvent = vi.fn().mockResolvedValue([]); const onChange = vi.fn(); render( , ); const input = await screen.findByTestId('field-input'); await waitFor(() => { expect(model.props.onChange).toBeTypeOf('function'); }); fireEvent.change(input, { target: { value: 'abc' } }); expect(model.props.value).toBe('abc'); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith('abc'); }); it('updates model and form value when field components pass values directly', async () => { const flowEngine = new FlowEngine(); const model = new InputModel({ uid: 'direct-value-field-model', flowEngine, props: { value: '', }, }); model.dispatchEvent = vi.fn().mockResolvedValue([]); const onChange = vi.fn(); render( , ); await screen.findByTestId('field-input'); await waitFor(() => { expect(model.props.onChange).toBeTypeOf('function'); }); act(() => { model.props.onChange(123); }); expect(model.props.value).toBe(123); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(123); }); it('defers model and form value updates while IME composition is active', async () => { const flowEngine = new FlowEngine(); const model = new InputModel({ uid: 'input-field-model', flowEngine, props: { value: '', }, }); model.dispatchEvent = vi.fn().mockResolvedValue([]); const onChange = vi.fn(); render( , ); const input = await screen.findByTestId('field-input'); await waitFor(() => { expect(model.props.onChange).toBeTypeOf('function'); }); fireEvent.compositionStart(input); fireEvent.change(input, { target: { value: 't' }, nativeEvent: { isComposing: true } }); expect(model.props.value).toBe(''); expect(onChange).not.toHaveBeenCalled(); fireEvent.compositionEnd(input, { target: { value: 'te' } }); expect(model.props.value).toBe('te'); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith('te'); }); it('does not commit composition value when composition end opts out', async () => { const flowEngine = new FlowEngine(); const model = new InputModel({ uid: 'composition-opt-out-field-model', flowEngine, props: { value: '', }, }); model.dispatchEvent = vi.fn().mockResolvedValue([]); const onChange = vi.fn(); render( , ); await screen.findByTestId('field-input'); await waitFor(() => { expect(model.props.onChange).toBeTypeOf('function'); expect(model.props.onCompositionStart).toBeTypeOf('function'); expect(model.props.onCompositionEnd).toBeTypeOf('function'); }); act(() => { model.props.onCompositionStart(); model.props.onChange({ target: { value: 'search' }, nativeEvent: { isComposing: true } }); model.props.onCompositionEnd({ target: { value: 'search' } }, false); }); expect(model.props.value).toBe(''); expect(onChange).not.toHaveBeenCalled(); }); });