mirror of
https://github.com/nocobase/nocobase.git
synced 2026-08-29 02:03:53 +08:00
fix(client-v2): fix IME input for text fields (#9764)
This commit is contained in:
@@ -9,18 +9,64 @@
|
||||
|
||||
import { EditableItemModel, FilterableItemModel, tExpr } from '@nocobase/flow-engine';
|
||||
import { Input } from 'antd';
|
||||
import React from 'react';
|
||||
import type { InputProps, InputRef } from 'antd';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { customAlphabet as Alphabet } from 'nanoid';
|
||||
import { FieldModel } from '../base/FieldModel';
|
||||
import { ScanInput } from '../../../components/form/ScanInput';
|
||||
|
||||
function IMESafeInput(props: InputProps) {
|
||||
const { value, onChange, onCompositionStart, onCompositionEnd, ...rest } = props;
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const previousValueRef = useRef(value);
|
||||
const defaultValue = typeof value === 'bigint' ? String(value) : value;
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.is(previousValueRef.current, value)) {
|
||||
return;
|
||||
}
|
||||
previousValueRef.current = value;
|
||||
|
||||
const input = inputRef.current?.input;
|
||||
if (input) {
|
||||
const nextValue = value == null ? '' : String(value);
|
||||
if (input.value !== nextValue) {
|
||||
input.value = nextValue;
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const getEventValue = (event: React.ChangeEvent<HTMLInputElement> | React.CompositionEvent<HTMLInputElement>) =>
|
||||
event.currentTarget.value;
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...rest}
|
||||
ref={inputRef}
|
||||
defaultValue={defaultValue}
|
||||
onChange={(event) => {
|
||||
previousValueRef.current = getEventValue(event);
|
||||
onChange?.(event);
|
||||
}}
|
||||
onCompositionStart={(event) => {
|
||||
previousValueRef.current = getEventValue(event);
|
||||
onCompositionStart?.(event);
|
||||
}}
|
||||
onCompositionEnd={(event) => {
|
||||
previousValueRef.current = getEventValue(event);
|
||||
onCompositionEnd?.(event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export class InputFieldModel extends FieldModel {
|
||||
render() {
|
||||
if (this.props.enableScan) {
|
||||
return <ScanInput {...this.props} />;
|
||||
}
|
||||
const { enableScan, disableManualInput, ...inputProps } = this.props;
|
||||
return <Input {...inputProps} />;
|
||||
return <IMESafeInput {...inputProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,61 @@
|
||||
*/
|
||||
|
||||
import { Input } from 'antd';
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import type { TextAreaProps } from 'antd/es/input';
|
||||
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { largeField, EditableItemModel } from '@nocobase/flow-engine';
|
||||
import { FieldModel } from '../base/FieldModel';
|
||||
|
||||
function IMESafeTextArea(props: TextAreaProps) {
|
||||
const { value, onChange, onCompositionStart, onCompositionEnd, ...rest } = props;
|
||||
const textAreaRef = useRef<TextAreaRef>(null);
|
||||
const previousValueRef = useRef(value);
|
||||
const defaultValue = typeof value === 'bigint' ? String(value) : value;
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.is(previousValueRef.current, value)) {
|
||||
return;
|
||||
}
|
||||
previousValueRef.current = value;
|
||||
|
||||
const textArea = textAreaRef.current?.resizableTextArea?.textArea;
|
||||
if (textArea) {
|
||||
const nextValue = value == null ? '' : String(value);
|
||||
if (textArea.value !== nextValue) {
|
||||
textArea.value = nextValue;
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const getEventValue = (event: React.ChangeEvent<HTMLTextAreaElement> | React.CompositionEvent<HTMLTextAreaElement>) =>
|
||||
event.currentTarget.value;
|
||||
|
||||
return (
|
||||
<Input.TextArea
|
||||
{...rest}
|
||||
ref={textAreaRef}
|
||||
defaultValue={defaultValue}
|
||||
onChange={(event) => {
|
||||
previousValueRef.current = getEventValue(event);
|
||||
onChange?.(event);
|
||||
}}
|
||||
onCompositionStart={(event) => {
|
||||
previousValueRef.current = getEventValue(event);
|
||||
onCompositionStart?.(event);
|
||||
}}
|
||||
onCompositionEnd={(event) => {
|
||||
previousValueRef.current = getEventValue(event);
|
||||
onCompositionEnd?.(event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@largeField()
|
||||
export class TextareaFieldModel extends FieldModel {
|
||||
render() {
|
||||
return <Input.TextArea {...this.props} />;
|
||||
return <IMESafeTextArea {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { FlowEngine } from '@nocobase/flow-engine';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { InputFieldModel } from '../InputFieldModel';
|
||||
@@ -43,6 +43,105 @@ describe('InputFieldModel', () => {
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
it('keeps IME composition text visible before the parent value is committed', () => {
|
||||
const onChange = vi.fn();
|
||||
const onCompositionStart = vi.fn();
|
||||
const onCompositionEnd = vi.fn();
|
||||
const model = createInputFieldModel({
|
||||
value: '',
|
||||
onChange,
|
||||
onCompositionStart,
|
||||
onCompositionEnd,
|
||||
});
|
||||
|
||||
render(<>{model.render()}</>);
|
||||
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
|
||||
fireEvent.compositionStart(input);
|
||||
fireEvent.change(input, { target: { value: 'ni' }, nativeEvent: { isComposing: true } });
|
||||
|
||||
expect(input.value).toBe('ni');
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onCompositionStart).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.compositionEnd(input, { target: { value: '你' } });
|
||||
|
||||
expect(input.value).toBe('你');
|
||||
expect(onCompositionEnd).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns to the committed parent value after IME composition is submitted', () => {
|
||||
const onChange = vi.fn();
|
||||
const onCompositionEnd = vi.fn();
|
||||
const createModel = (value: string) =>
|
||||
createInputFieldModel({
|
||||
value,
|
||||
onChange,
|
||||
onCompositionEnd,
|
||||
});
|
||||
|
||||
const { rerender } = render(<>{createModel('').render()}</>);
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
|
||||
fireEvent.compositionStart(input);
|
||||
fireEvent.change(input, { target: { value: 'ni' }, nativeEvent: { isComposing: true } });
|
||||
fireEvent.compositionEnd(input, { target: { value: '你' } });
|
||||
|
||||
expect(input.value).toBe('你');
|
||||
|
||||
rerender(<>{createModel('你').render()}</>);
|
||||
|
||||
expect(input.value).toBe('你');
|
||||
});
|
||||
|
||||
it('does not overwrite in-progress DOM input with a stale parent value', async () => {
|
||||
const onChange = vi.fn();
|
||||
const createModel = (value: string) =>
|
||||
createInputFieldModel({
|
||||
value,
|
||||
onChange,
|
||||
});
|
||||
|
||||
const { rerender } = render(<>{createModel('').render()}</>);
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
|
||||
fireEvent.change(input, { target: { value: 'té' } });
|
||||
rerender(<>{createModel('').render()}</>);
|
||||
|
||||
expect(input.value).toBe('té');
|
||||
|
||||
rerender(<>{createModel('reset').render()}</>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect((screen.getByRole('textbox') as HTMLInputElement).value).toBe('reset');
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps replacement-style Vietnamese IME edits in the DOM', () => {
|
||||
const onChange = vi.fn();
|
||||
const createModel = (value: string) =>
|
||||
createInputFieldModel({
|
||||
value,
|
||||
onChange,
|
||||
});
|
||||
|
||||
const { rerender } = render(<>{createModel('').render()}</>);
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
|
||||
fireEvent.change(input, { target: { value: 'te' } });
|
||||
rerender(<>{createModel('te').render()}</>);
|
||||
expect(input.value).toBe('te');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'té' } });
|
||||
rerender(<>{createModel('te').render()}</>);
|
||||
expect(input.value).toBe('té');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'tét' } });
|
||||
rerender(<>{createModel('té').render()}</>);
|
||||
expect(input.value).toBe('tét');
|
||||
});
|
||||
|
||||
it('renders ScanInput when scan input is enabled', () => {
|
||||
const model = createInputFieldModel({ enableScan: true });
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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 { FlowEngine } from '@nocobase/flow-engine';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { TextareaFieldModel } from '../TextareaFieldModel';
|
||||
|
||||
function createTextareaFieldModel(props?: Record<string, unknown>) {
|
||||
const engine = new FlowEngine();
|
||||
return new TextareaFieldModel({
|
||||
uid: 'textarea-field-model-test',
|
||||
flowEngine: engine,
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
describe('TextareaFieldModel', () => {
|
||||
it('keeps IME composition text visible before the parent value is committed', () => {
|
||||
const onChange = vi.fn();
|
||||
const onCompositionStart = vi.fn();
|
||||
const onCompositionEnd = vi.fn();
|
||||
const model = createTextareaFieldModel({
|
||||
value: '',
|
||||
onChange,
|
||||
onCompositionStart,
|
||||
onCompositionEnd,
|
||||
});
|
||||
|
||||
render(<>{model.render()}</>);
|
||||
|
||||
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.compositionStart(textarea);
|
||||
fireEvent.change(textarea, { target: { value: 'ni' }, nativeEvent: { isComposing: true } });
|
||||
|
||||
expect(textarea.value).toBe('ni');
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onCompositionStart).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.compositionEnd(textarea, { target: { value: '你' } });
|
||||
|
||||
expect(textarea.value).toBe('你');
|
||||
expect(onCompositionEnd).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not overwrite in-progress DOM input with a stale parent value', async () => {
|
||||
const onChange = vi.fn();
|
||||
const createModel = (value: string) =>
|
||||
createTextareaFieldModel({
|
||||
value,
|
||||
onChange,
|
||||
});
|
||||
|
||||
const { rerender } = render(<>{createModel('').render()}</>);
|
||||
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(textarea, { target: { value: 'té' } });
|
||||
rerender(<>{createModel('').render()}</>);
|
||||
|
||||
expect(textarea.value).toBe('té');
|
||||
|
||||
rerender(<>{createModel('reset').render()}</>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect((screen.getByRole('textbox') as HTMLTextAreaElement).value).toBe('reset');
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps replacement-style Vietnamese IME edits in the DOM', () => {
|
||||
const onChange = vi.fn();
|
||||
const createModel = (value: string) =>
|
||||
createTextareaFieldModel({
|
||||
value,
|
||||
onChange,
|
||||
});
|
||||
|
||||
const { rerender } = render(<>{createModel('').render()}</>);
|
||||
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(textarea, { target: { value: 'te' } });
|
||||
rerender(<>{createModel('te').render()}</>);
|
||||
expect(textarea.value).toBe('te');
|
||||
|
||||
fireEvent.change(textarea, { target: { value: 'té' } });
|
||||
rerender(<>{createModel('te').render()}</>);
|
||||
expect(textarea.value).toBe('té');
|
||||
|
||||
fireEvent.change(textarea, { target: { value: 'tét' } });
|
||||
rerender(<>{createModel('té').render()}</>);
|
||||
expect(textarea.value).toBe('tét');
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@
|
||||
import type { FlowModelRendererProps } from './FlowModelRenderer';
|
||||
import { FlowModelRenderer } from './FlowModelRenderer';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
const flowModelRendererPropKeys: (keyof FlowModelRendererProps)[] = [
|
||||
'model',
|
||||
@@ -28,49 +28,63 @@ const flowModelRendererPropKeys: (keyof FlowModelRendererProps)[] = [
|
||||
];
|
||||
|
||||
export function FieldModelRenderer(props: any) {
|
||||
const { model, ...rest } = props;
|
||||
const { model, onChange, ...rest } = props;
|
||||
const composingRef = useRef(false);
|
||||
|
||||
const handleChange = (e: any) => {
|
||||
let val;
|
||||
if (e && e.target && typeof e.target.value !== 'undefined') {
|
||||
val = e.target.value;
|
||||
} else if (
|
||||
typeof e === 'string' ||
|
||||
typeof e === 'number' ||
|
||||
typeof e === 'boolean' ||
|
||||
(typeof e === 'object' && !(e instanceof Event))
|
||||
) {
|
||||
val = e;
|
||||
} else {
|
||||
val = null;
|
||||
}
|
||||
const handleChange = useCallback(
|
||||
(e: any) => {
|
||||
let val;
|
||||
if (e && e.target && typeof e.target.value !== 'undefined') {
|
||||
val = e.target.value;
|
||||
} else if (
|
||||
typeof e === 'string' ||
|
||||
typeof e === 'number' ||
|
||||
typeof e === 'boolean' ||
|
||||
(typeof e === 'object' && !(e instanceof Event))
|
||||
) {
|
||||
val = e;
|
||||
} else {
|
||||
val = null;
|
||||
}
|
||||
|
||||
model.setProps({ value: val });
|
||||
if (!composingRef.current) {
|
||||
props.onChange?.(val);
|
||||
}
|
||||
};
|
||||
const handleCompositionStart = () => {
|
||||
const isComposing = composingRef.current || e?.nativeEvent?.isComposing;
|
||||
if (isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.setProps({ value: val });
|
||||
if (!composingRef.current) {
|
||||
onChange?.(val);
|
||||
}
|
||||
},
|
||||
[model, onChange],
|
||||
);
|
||||
const handleCompositionStart = useCallback(() => {
|
||||
composingRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCompositionEnd = (e: React.CompositionEvent<HTMLInputElement>, flag = true) => {
|
||||
composingRef.current = false;
|
||||
if (flag) {
|
||||
props.onChange(e);
|
||||
}
|
||||
};
|
||||
const handleCompositionEnd = useCallback(
|
||||
(e: React.CompositionEvent<HTMLInputElement>, flag = true) => {
|
||||
composingRef.current = false;
|
||||
if (flag) {
|
||||
handleChange(e);
|
||||
}
|
||||
},
|
||||
[handleChange],
|
||||
);
|
||||
|
||||
const modelProps = {
|
||||
onCompositionStart: handleCompositionStart,
|
||||
onCompositionEnd: handleCompositionEnd,
|
||||
..._.omit(rest, flowModelRendererPropKeys),
|
||||
onChange: handleChange,
|
||||
};
|
||||
const modelProps = useMemo(
|
||||
() => ({
|
||||
onCompositionStart: handleCompositionStart,
|
||||
onCompositionEnd: handleCompositionEnd,
|
||||
..._.omit(rest, flowModelRendererPropKeys),
|
||||
onChange: handleChange,
|
||||
}),
|
||||
[handleChange, handleCompositionEnd, handleCompositionStart, rest],
|
||||
);
|
||||
useEffect(() => {
|
||||
model && model.setProps(modelProps);
|
||||
}, [modelProps]);
|
||||
}, [model, modelProps]);
|
||||
|
||||
return <FlowModelRenderer model={model} {...rest} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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 (
|
||||
<input
|
||||
data-testid="field-input"
|
||||
value={this.props.value ?? ''}
|
||||
onChange={this.props.onChange}
|
||||
onCompositionStart={this.props.onCompositionStart}
|
||||
onCompositionEnd={this.props.onCompositionEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
<FlowEngineProvider engine={flowEngine}>
|
||||
<FieldModelRenderer model={model} onChange={onChange} />
|
||||
</FlowEngineProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FlowEngineProvider engine={flowEngine}>
|
||||
<FieldModelRenderer model={model} onChange={onChange} />
|
||||
</FlowEngineProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FlowEngineProvider engine={flowEngine}>
|
||||
<FieldModelRenderer model={model} onChange={onChange} />
|
||||
</FlowEngineProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FlowEngineProvider engine={flowEngine}>
|
||||
<FieldModelRenderer model={model} onChange={onChange} />
|
||||
</FlowEngineProvider>,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user