merge e2e into master

This commit is contained in:
yupeng12 2024-03-05 19:16:11 +08:00
commit a7c550ac2f
100 changed files with 1057 additions and 367 deletions

View File

@ -246,6 +246,7 @@ export default function (schema, schemaProps, showCode, envOverrides) {
};
});
},
enableTestid: true,
...envOverrides
};

View File

@ -16,7 +16,7 @@ import {IScopedContext, ScopedContext} from './Scoped';
import {Schema, SchemaNode} from './types';
import {DebugWrapper} from './utils/debug';
import getExprProperties from './utils/filter-schema';
import {anyChanged, chainEvents, autobind} from './utils/helper';
import {anyChanged, chainEvents, autobind, TestIdBuilder} from './utils/helper';
import {SimpleMap} from './utils/SimpleMap';
import {bindEvent, dispatchEvent, RendererEvent} from './utils/renderer-event';
import {isAlive} from 'mobx-state-tree';
@ -476,8 +476,14 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
(props as any).static = isStatic;
}
if (rest.env.enableTestid && props.id && !props.testid) {
props.testid = props.id;
// 优先使用组件自己的testid或者id这个解决不了table行内的一些子元素
// 每一行都会出现这个testid的元素只在测试工具中直接使用nth拿序号
if (props.testid || props.id || props.testIdBuilder == null) {
if (!(props.testIdBuilder instanceof TestIdBuilder)) {
props.testIdBuilder = new TestIdBuilder(
rest.env.enableTestid ? props.testid || props.id : null
);
}
}
// 自动解析变量模式,主要是方便直接引入第三方组件库,无需为了支持变量封装一层

View File

@ -9,7 +9,8 @@ import {
qsparse,
string2regExp,
parseQuery,
isMobile
isMobile,
TestIdBuilder
} from './utils/helper';
import {
fetcherResult,
@ -72,6 +73,7 @@ export interface RendererProps
env: RendererEnv;
$path: string; // 当前组件所在的层级信息
$schema: any; // 原始 schema 配置
testIdBuild: TestIdBuilder;
store?: IIRendererStore;
syncSuperStore?: boolean;
data: {

View File

@ -49,7 +49,7 @@ import LazyComponent from '../components/LazyComponent';
import {isAlive} from 'mobx-state-tree';
import type {LabelAlign} from './Item';
import {buildTestId, injectObjectChain} from '../utils';
import {injectObjectChain} from '../utils';
import {reaction} from 'mobx';
import groupBy from 'lodash/groupBy';
@ -1860,7 +1860,6 @@ export default class Form extends React.Component<FormProps, object> {
)}
onSubmit={this.handleFormSubmit}
noValidate
{...buildTestId(testid)}
>
{/* 实现回车自动提交 */}
<input type="submit" style={{display: 'none'}} />

View File

@ -552,7 +552,6 @@ export interface FormItemProps extends RendererProps {
// error string
error?: string;
showErrorMsg?: boolean;
testid?: string;
}
// 下发下去的属性

View File

@ -2,6 +2,7 @@
import type {JSONSchema7} from 'json-schema';
import {ListenerAction} from './actions/Action';
import {debounceConfig, trackConfig} from './utils/renderer-event';
import type {TestIdBuilder} from './utils/helper';
export interface Option {
/**
@ -693,6 +694,8 @@ export interface BaseSchemaWithoutType {
*
*/
useMobileUI?: boolean;
testIdBuilder?: TestIdBuilder;
}
export type OperatorType =

View File

@ -2282,18 +2282,49 @@ export function replaceUrlParams(path: string, params: Record<string, any>) {
export const TEST_ID_KEY: 'data-testid' = 'data-testid';
export function buildTestId(testid?: string, data?: PlainObject) {
if (!testid) {
return {};
}
return {
[TEST_ID_KEY]: filter(testid, data)
};
}
export class TestIdBuilder {
testId?: string;
export function getTestId(testid?: string, data?: PlainObject) {
if (!testid) {
return undefined;
static fast(testId: string) {
return {
[TEST_ID_KEY]: testId
};
}
// 为空就表示没有启用testId后续一直返回都将是空
constructor(testId?: string) {
this.testId = testId;
}
// 生成子区域的testid生成器
getChild(childPath: string | number, data?: object) {
if (this.testId == null) {
return new TestIdBuilder();
}
return new TestIdBuilder(
data
? filter(`${this.testId}-${childPath}`, data)
: `${this.testId}-${childPath}`
);
}
// 获取当前组件的testid
getTestId(data?: object) {
if (this.testId == null) {
return undefined;
}
return {
[TEST_ID_KEY]: data ? filter(this.testId, data) : this.testId
};
}
getTestIdValue(data?: object) {
if (this.testId == null) {
return undefined;
}
return data ? filter(this.testId, data) : this.testId;
}
return buildTestId(testid, data)[TEST_ID_KEY];
}

View File

@ -1,4 +1,4 @@
import {Html, render, TooltipWrapper, buildTestId} from 'amis';
import {Html, render, TestIdBuilder, TooltipWrapper} from 'amis';
import {observer} from 'mobx-react';
import React from 'react';
import cx from 'classnames';
@ -18,6 +18,7 @@ type PanelProps = {
};
searchRendererType: string;
className?: string;
testIdBuilder?: TestIdBuilder;
};
type PanelStates = {
@ -98,7 +99,7 @@ export default class RenderersPanel extends React.Component<
}
render() {
const {store, searchRendererType, className} = this.props;
const {store, searchRendererType, className, testIdBuilder} = this.props;
const grouped = this.props.groupedRenderers || {};
const keys = Object.keys(grouped);
@ -189,7 +190,7 @@ export default class RenderersPanel extends React.Component<
onDragStart={(e: React.DragEvent) =>
this.handleDragStart(e, item.name)
}
{...buildTestId(testid)}
{...testIdBuilder?.getChild(testid).getTestId()}
>
<div
className="icon-box"

View File

@ -30,6 +30,16 @@
color: var(--colors-neutral-text-6);
outline: none;
transition: border 0.24s ease-in-out;
position: relative;
> input {
display: block !important;
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
opacity: 0;
}
}
&-dropzone:focus {

View File

@ -5,8 +5,8 @@
*/
import React from 'react';
import {mapTree} from 'amis-core';
import {ClassNamesFn, themeable, buildTestId} from 'amis-core';
import {TestIdBuilder, mapTree} from 'amis-core';
import {ClassNamesFn, themeable} from 'amis-core';
export type LinkItem = LinkItemProps;
interface LinkItemProps {
@ -19,8 +19,8 @@ interface LinkItemProps {
children?: Array<LinkItem>;
path?: string;
icon?: string;
testid?: string;
component?: React.ElementType;
testIdBuilder?: TestIdBuilder;
}
export interface Navigation {
@ -56,7 +56,7 @@ interface AsideNavState {
export class AsideNav extends React.Component<AsideNavProps, AsideNavState> {
static defaultProps = {
renderLink: (item: LinkItemProps) => (
<a {...buildTestId(item.testid, item)}>{item.label}</a>
<a {...item.testIdBuilder?.getTestId()}>{item.label}</a>
),
renderSubLinks: (
link: LinkItemProps,

View File

@ -128,7 +128,8 @@ export class AssociatedSelection extends BaseSelection<
loadingConfig,
checkAll,
checkAllLabel,
deferField = 'defer'
deferField = 'defer',
testIdBuilder
} = this.props;
const selectdOption = BaseSelection.resolveSelected(
@ -152,6 +153,7 @@ export class AssociatedSelection extends BaseSelection<
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
loadingConfig={loadingConfig}
testIdBuilder={testIdBuilder?.getChild('left-selection')}
/>
) : (
<GroupedSelection
@ -164,6 +166,7 @@ export class AssociatedSelection extends BaseSelection<
clearable={false}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
testIdBuilder={testIdBuilder?.getChild('left-selection')}
/>
)}
</div>
@ -204,6 +207,7 @@ export class AssociatedSelection extends BaseSelection<
multiple={multiple}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
testIdBuilder={testIdBuilder?.getChild('right-selection')}
/>
) : rightMode === 'tree' ? (
<Tree
@ -218,6 +222,7 @@ export class AssociatedSelection extends BaseSelection<
loadingConfig={loadingConfig}
checkAllLabel={checkAllLabel}
checkAll={checkAll}
testIdBuilder={testIdBuilder?.getChild('right-selection')}
/>
) : rightMode === 'chained' ? (
<ChainedSelection
@ -234,6 +239,7 @@ export class AssociatedSelection extends BaseSelection<
loadingConfig={loadingConfig}
checkAllLabel={checkAllLabel}
checkAll={checkAll}
testIdBuilder={testIdBuilder?.getChild('right-selection')}
/>
) : (
<GroupedSelection
@ -249,6 +255,7 @@ export class AssociatedSelection extends BaseSelection<
itemHeight={itemHeight}
checkAllLabel={checkAllLabel}
checkAll={checkAll}
testIdBuilder={testIdBuilder?.getChild('right-selection')}
/>
)
) : (

View File

@ -8,6 +8,7 @@ import {ClassNamesFn, themeable, ThemeProps} from 'amis-core';
import {RootClose} from 'amis-core';
import {removeHTMLTag} from 'amis-core';
import {Icon} from './icons';
import type {TestIdBuilder} from 'amis-core';
export type ItemPlace = 'start' | 'middle' | 'end';
export type TooltipPositionType = 'top' | 'bottom' | 'left' | 'right';
@ -32,6 +33,7 @@ interface BreadcrumbItemProps {
tooltipContainer?: any;
tooltipPosition?: TooltipPositionType;
classnames: ClassNamesFn;
testIdBuilder?: TestIdBuilder;
[propName: string]: any;
}
@ -39,6 +41,7 @@ interface BreadcrumbProps extends ThemeProps {
tooltipContainer?: any;
tooltipPosition?: TooltipPositionType;
items: Array<BreadcrumbBaseItem>;
testIdBuilder?: TestIdBuilder;
[propName: string]: any;
}
@ -65,6 +68,7 @@ export class Breadcrumb extends React.Component<BreadcrumbProps> {
separatorClassName,
items,
separator,
testIdBuilder,
...restProps
} = this.props;
@ -75,6 +79,9 @@ export class Breadcrumb extends React.Component<BreadcrumbProps> {
const crumbs = items
.map<React.ReactNode>((item, index) => {
const itemTestIdBuilder = testIdBuilder?.getChild(
`item-${item.label || index}`
);
let itemPlace: ItemPlace = 'middle';
if (index === 0) {
itemPlace = 'start';
@ -88,6 +95,7 @@ export class Breadcrumb extends React.Component<BreadcrumbProps> {
item={item}
itemPlace={itemPlace}
key={index}
testIdBuilder={itemTestIdBuilder}
></BreadcrumbItem>
);
})
@ -157,7 +165,12 @@ export class BreadcrumbItem extends React.Component<
item: BreadcrumbBaseItem,
label?: string
) {
const {itemClassName, dropdownItemClassName, classnames: cx} = this.props;
const {
itemClassName,
dropdownItemClassName,
classnames: cx,
testIdBuilder
} = this.props;
const baseItemClassName =
itemType === 'default' ? itemClassName : dropdownItemClassName;
if (showHref) {
@ -165,6 +178,7 @@ export class BreadcrumbItem extends React.Component<
<a
href={item.href}
className={cx('Breadcrumb-item-' + itemType, baseItemClassName)}
{...testIdBuilder?.getTestId()}
>
{item.icon ? (
<Icon
@ -179,7 +193,10 @@ export class BreadcrumbItem extends React.Component<
);
}
return (
<span className={cx('Breadcrumb-item-' + itemType, baseItemClassName)}>
<span
className={cx('Breadcrumb-item-' + itemType, baseItemClassName)}
{...testIdBuilder?.getTestId()}
>
{item.icon ? (
<Icon
cx={cx}

View File

@ -5,8 +5,8 @@
import React from 'react';
import TooltipWrapper, {TooltipObject, Trigger} from './TooltipWrapper';
import {pickEventsProps} from 'amis-core';
import {ClassNamesFn, themeable, buildTestId} from 'amis-core';
import {TestIdBuilder, pickEventsProps} from 'amis-core';
import {ClassNamesFn, themeable} from 'amis-core';
import Spinner, {SpinnerExtraProps} from './Spinner';
export interface ButtonProps
@ -14,7 +14,6 @@ export interface ButtonProps
SpinnerExtraProps {
id?: string;
className?: string;
testid?: string;
style?: any;
href?: string;
title?: string;
@ -37,6 +36,7 @@ export interface ButtonProps
overrideClassName?: boolean;
loading?: boolean;
loadingClassName?: string;
testIdBuilder?: TestIdBuilder;
}
export class Button extends React.Component<ButtonProps> {
@ -78,7 +78,7 @@ export class Button extends React.Component<ButtonProps> {
loadingClassName,
overrideClassName,
loadingConfig,
testid,
testIdBuilder,
...rest
} = this.props;
@ -94,7 +94,7 @@ export class Button extends React.Component<ButtonProps> {
{...pickEventsProps(rest)}
onClick={rest.onClick && disabled ? () => {} : rest.onClick}
href={href}
{...buildTestId(testid)}
{...testIdBuilder?.getTestId()}
className={cx(
overrideClassName
? ''

View File

@ -72,9 +72,11 @@ export class ChainedSelection extends BaseSelection<
itemClassName,
itemRender,
multiple,
labelField
labelField,
testIdBuilder
} = this.props;
const valueArray = this.valueArray;
const itemTIB = testIdBuilder?.getChild(`item-${option.value || index}`);
return (
<div
@ -96,6 +98,7 @@ export class ChainedSelection extends BaseSelection<
disabled={disabled || option.disabled}
labelClassName={labelClassName}
description={option.description}
testIdBuilder={itemTIB}
/>
) : null}
@ -130,9 +133,11 @@ export class ChainedSelection extends BaseSelection<
multiple,
labelField,
deferField = 'defer',
loadingConfig
loadingConfig,
testIdBuilder
} = this.props;
const valueArray = this.valueArray;
const itemTIB = testIdBuilder?.getChild(`item-${option.value || index}`);
if (Array.isArray(option.children) || option[deferField]) {
return (
@ -147,6 +152,7 @@ export class ChainedSelection extends BaseSelection<
~this.state.selected.indexOf(id) ? 'is-active' : ''
)}
onClick={() => this.selectOption(option, depth, id)}
{...itemTIB?.getTestId()}
>
<div className={cx('ChainedSelection-itemLabel')}>
{itemRender(option, {
@ -230,7 +236,8 @@ export class ChainedSelection extends BaseSelection<
translate: __,
virtualThreshold = 1000,
itemHeight = 32,
virtualListHeight
virtualListHeight,
testIdBuilder
} = this.props;
this.valueArray = BaseSelection.value2array(value, options, option2value);

View File

@ -4,7 +4,7 @@
*/
import React from 'react';
import {ThemeProps, themeable} from 'amis-core';
import {TestIdBuilder, ThemeProps, themeable} from 'amis-core';
import {autobind} from 'amis-core';
const preventEvent = (e: any) => e.stopPropagation();
@ -28,6 +28,7 @@ interface CheckboxProps extends ThemeProps {
partial?: boolean;
optionType?: 'default' | 'button';
children?: React.ReactNode | Array<React.ReactNode>;
testIdBuilder?: TestIdBuilder;
}
export class Checkbox extends React.Component<CheckboxProps, any> {
@ -72,7 +73,8 @@ export class Checkbox extends React.Component<CheckboxProps, any> {
name,
labelClassName,
optionType,
mobileUI
mobileUI,
testIdBuilder
} = this.props;
const _checked =
typeof checked !== 'undefined'
@ -96,6 +98,7 @@ export class Checkbox extends React.Component<CheckboxProps, any> {
'is-mobile': mobileUI
})}
data-role="checkbox"
{...testIdBuilder?.getTestId()}
>
<input
type={type}
@ -114,8 +117,13 @@ export class Checkbox extends React.Component<CheckboxProps, any> {
readOnly={readOnly}
name={name}
/>
<i />
<span className={cx(labelClassName)}>{children || label}</span>
<i {...testIdBuilder?.getChild('input').getTestId()} />
<span
className={cx(labelClassName)}
{...testIdBuilder?.getChild('label').getTestId()}
>
{children || label}
</span>
{description ? (
<div className={cx('Checkbox-desc')}>{description}</div>
) : null}

View File

@ -27,7 +27,7 @@ import Input from './Input';
import Button from './Button';
import type {Moment} from 'moment';
import type {PlainObject, RendererEnv} from 'amis-core';
import type {PlainObject, RendererEnv, TestIdBuilder} from 'amis-core';
import type {ChangeEventViewMode, MutableUnitOfTime} from './calendar/Calendar';
const availableShortcuts: {[propName: string]: any} = {
@ -334,6 +334,7 @@ export interface DateProps extends LocaleProps, ThemeProps {
// 是否为结束时间
isEndDate?: boolean;
testIdBuilder?: TestIdBuilder;
disabledDate?: (date: moment.Moment) => any;
onClick?: (date: moment.Moment) => any;
@ -949,6 +950,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
mobileCalendarMode,
label,
env,
testIdBuilder,
onClick,
onMouseEnter,
onMouseLeave,
@ -1058,6 +1060,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
testIdBuilder={testIdBuilder?.getChild('calendar')}
/>
</div>
);
@ -1081,6 +1084,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
)}
ref={this.domRef}
onClick={this.handleClick}
{...testIdBuilder?.getTestId()}
>
<Input
className={cx('DatePicker-input')}
@ -1092,17 +1096,25 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
value={this.state.inputValue || ''}
disabled={disabled}
readOnly={mobileUI}
{...testIdBuilder?.getChild('input').getTestId()}
/>
{clearable &&
!disabled &&
normalizeDate(value, valueFormat || format) ? (
<a className={cx(`DatePicker-clear`)} onClick={this.clearValue}>
<a
className={cx(`DatePicker-clear`)}
onClick={this.clearValue}
{...testIdBuilder?.getChild('clear').getTestId()}
>
<Icon icon="input-clear" className="icon" />
</a>
) : null}
<a className={cx(`DatePicker-toggler`)}>
<a
className={cx(`DatePicker-toggler`)}
{...testIdBuilder?.getChild('toggler').getTestId()}
>
<Icon
icon={viewMode === 'time' ? 'clock' : 'date'}
className="icon"
@ -1151,6 +1163,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
testIdBuilder={testIdBuilder?.getChild('calendar')}
// utc={utc}
/>
{isConfirmMode ? (
@ -1209,6 +1222,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
testIdBuilder={testIdBuilder?.getChild('calendar')}
// utc={utc}
/>
</PopUp>

View File

@ -38,6 +38,7 @@ import type {
MutableUnitOfTime,
ChangeEventViewStatus
} from './calendar/Calendar';
import type {TestIdBuilder} from 'amis-core';
export interface DateRangePickerProps extends ThemeProps, LocaleProps {
className?: string;
@ -86,6 +87,7 @@ export interface DateRangePickerProps extends ThemeProps, LocaleProps {
animation?: boolean;
/** 日期处理函数,通常用于自定义处理绑定日期的值 */
transform?: string;
testIdBuilder?: TestIdBuilder;
}
export interface DateRangePickerState {
@ -1328,14 +1330,21 @@ export class DateRangePicker extends React.Component<
if (!shortcuts) {
return null;
}
const {classPrefix: ns, format, valueFormat, data} = this.props;
const {
classPrefix: ns,
format,
valueFormat,
data,
translate: __,
testIdBuilder
} = this.props;
let shortcutArr: Array<string | ShortCuts>;
if (typeof shortcuts === 'string') {
shortcutArr = shortcuts.split(',');
} else {
shortcutArr = shortcuts;
}
const __ = this.props.translate;
const TIDBuilder = testIdBuilder?.getChild('shortcut');
return (
<ul className={`${ns}DateRangePicker-rangers`}>
@ -1407,7 +1416,9 @@ export class DateRangePicker extends React.Component<
onClick={() => this.selectShortcut(shortcut)}
key={index}
>
<a>{__(shortcut.label)}</a>
<a {...TIDBuilder?.getChild(shortcut.key).getTestId()}>
{__(shortcut.label)}
</a>
</li>
);
} else {
@ -1537,6 +1548,7 @@ export class DateRangePicker extends React.Component<
renderDay(props: any, currentDate: moment.Moment) {
let {startDate, endDate} = this.state;
const {testIdBuilder} = this.props;
if (
startDate &&
@ -1567,7 +1579,9 @@ export class DateRangePicker extends React.Component<
return (
<td {...omit(props, ['todayActiveStyle'])} {...others}>
<span>{currentDate.date()}</span>
<span {...testIdBuilder?.getChild(props.key)?.getTestId()}>
{currentDate.date()}
</span>
</td>
);
}
@ -1576,7 +1590,7 @@ export class DateRangePicker extends React.Component<
const currentDate = props.viewDate.year(year).month(month);
const {startDate, endDate} = this.state;
const {translate: __} = this.props;
const {translate: __, testIdBuilder} = this.props;
const monthStr = currentDate.format(__('MMM'));
const strLength = 3;
// Because some months are up to 5 characters long, we want to
@ -1612,7 +1626,9 @@ export class DateRangePicker extends React.Component<
return (
<td {...omit(props, 'viewDate')} {...others}>
<span>{monthStrFixedLength}</span>
<span {...testIdBuilder?.getChild(props.key).getTestId()}>
{monthStrFixedLength}
</span>
</td>
);
}
@ -1620,6 +1636,7 @@ export class DateRangePicker extends React.Component<
renderQuarter(props: any, quarter: number, year: number) {
const currentDate = moment().year(year).quarter(quarter);
const {startDate, endDate} = this.state;
const {testIdBuilder} = this.props;
if (
startDate &&
@ -1650,13 +1667,16 @@ export class DateRangePicker extends React.Component<
return (
<td {...props} {...others}>
<span>Q{quarter}</span>
<span {...testIdBuilder?.getChild(props.key).getTestId()}>
Q{quarter}
</span>
</td>
);
}
renderYear(props: any, year: number) {
const currentDate = moment().year(year);
const {startDate, endDate} = this.state;
const {testIdBuilder} = this.props;
if (
startDate &&
@ -1687,7 +1707,7 @@ export class DateRangePicker extends React.Component<
return (
<td {...props} {...others}>
<span>{year}</span>
<span {...testIdBuilder?.getChild(props.key).getTestId()}>{year}</span>
</td>
);
}
@ -1705,7 +1725,8 @@ export class DateRangePicker extends React.Component<
type,
viewMode = 'days',
label,
mobileUI
mobileUI,
testIdBuilder
} = this.props;
const __ = this.props.translate;
const {startDate, endDate, editState, curDateFormat, curTimeFormat} =
@ -1793,6 +1814,7 @@ export class DateRangePicker extends React.Component<
timeRangeHeader="开始时间"
embed={embed}
status="start"
testIdBuilder={testIdBuilder?.getChild('calendar-start')}
/>
)}
{(!isTimeRange ||
@ -1826,6 +1848,7 @@ export class DateRangePicker extends React.Component<
timeRangeHeader="结束时间"
embed={embed}
status="end"
testIdBuilder={testIdBuilder?.getChild('calendar-end')}
/>
)}
</div>
@ -1964,7 +1987,8 @@ export class DateRangePicker extends React.Component<
ranges,
shortcuts,
label,
animation
animation,
testIdBuilder
} = this.props;
const useCalendarMobile =
mobileUI && ['days', 'months', 'quarters'].indexOf(viewMode) > -1;
@ -2059,6 +2083,7 @@ export class DateRangePicker extends React.Component<
value={this.state.startInputValue || ''}
disabled={disabled}
readOnly={mobileUI}
testIdBuilder={testIdBuilder?.getChild('start')}
/>
<span
className={cx('DateRangePicker-input-separator')}
@ -2079,6 +2104,7 @@ export class DateRangePicker extends React.Component<
value={this.state.endInputValue || ''}
disabled={disabled}
readOnly={mobileUI}
testIdBuilder={testIdBuilder?.getChild('end')}
/>
{/* 指示游标 */}

View File

@ -129,10 +129,12 @@ export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
itemClassName,
itemRender,
multiple,
labelField
labelField,
testIdBuilder
} = this.props;
const valueArray = this.valueArray;
const itemTIB = testIdBuilder?.getChild(`item-${option.value || index}`);
return (
<div
@ -146,6 +148,7 @@ export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
!!~valueArray.indexOf(option) ? 'is-active' : ''
)}
onClick={() => this.toggleOption(option)}
{...itemTIB?.getTestId()}
>
{multiple ? (
<Checkbox
@ -154,6 +157,7 @@ export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
disabled={disabled || option.disabled}
labelClassName={labelClassName}
description={option.description}
testIdBuilder={itemTIB?.getChild('checkbox')}
/>
) : null}
<div className={cx('GroupedSelection-itemLabel')}>

View File

@ -4,11 +4,12 @@
*
*/
import React from 'react';
import {autobind} from 'amis-core';
import {TestIdBuilder, autobind} from 'amis-core';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
forwardedRef: React.Ref<HTMLInputElement>;
testIdBuilder?: TestIdBuilder;
}
export interface InputState {
@ -60,7 +61,7 @@ class InputInner extends React.Component<InputProps, InputState> {
}
render() {
const {forwardedRef, ...rest} = this.props;
const {forwardedRef, testIdBuilder, ...rest} = this.props;
return (
<input
@ -73,6 +74,7 @@ class InputInner extends React.Component<InputProps, InputState> {
onCompositionStart={this.handleComposition}
onCompositionUpdate={this.handleComposition}
onCompositionEnd={this.handleComposition}
{...testIdBuilder?.getTestId()}
/>
);
}
@ -81,5 +83,6 @@ class InputInner extends React.Component<InputProps, InputState> {
export default React.forwardRef<HTMLInputElement>((props, ref) => {
return <InputInner {...props} forwardedRef={ref} />;
}) as React.ComponentType<
React.InputHTMLAttributes<HTMLInputElement> & {ref?: any}
Omit<InputProps, 'forwardedRef'> &
React.InputHTMLAttributes<HTMLInputElement> & {ref?: any}
>;

View File

@ -1,5 +1,5 @@
import React from 'react';
import {ThemeProps, buildTestId, themeable} from 'amis-core';
import {TestIdBuilder, ThemeProps, themeable} from 'amis-core';
import Input from './Input';
import {autobind, ucFirst} from 'amis-core';
import {Icon} from './icons';
@ -21,7 +21,7 @@ export interface InputBoxProps
prefix?: JSX.Element;
children?: React.ReactNode | Array<React.ReactNode>;
borderMode?: 'full' | 'half' | 'none';
testid?: string;
testIdBuilder?: TestIdBuilder;
}
export interface InputBoxState {
@ -88,7 +88,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
borderMode,
onClick,
mobileUI,
testid,
testIdBuilder,
...rest
} = this.props;
const isFocused = this.state.isFocused;
@ -116,7 +116,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
onBlur={this.handleBlur}
size={12}
disabled={disabled}
{...buildTestId(testid)}
{...testIdBuilder?.getTestId()}
/>
{children}

View File

@ -2,6 +2,7 @@ import {ThemeProps, themeable} from 'amis-core';
import React from 'react';
import {Options, Option} from 'amis-core';
import {LocaleProps, localeable} from 'amis-core';
import type {TestIdBuilder} from 'amis-core';
export interface ListMenuProps extends ThemeProps, LocaleProps {
options: Options;
@ -14,6 +15,7 @@ export interface ListMenuProps extends ThemeProps, LocaleProps {
getItemProps: (props: {item: Option; index: number}) => any;
prefix?: JSX.Element;
children?: React.ReactNode | Array<React.ReactNode>;
testIdBuilder?: TestIdBuilder;
}
interface RenderResult {
@ -37,7 +39,8 @@ export class ListMenu extends React.Component<ListMenuProps> {
highlightIndex,
selectedOptions,
mobileUI,
onSelect
onSelect,
testIdBuilder
} = this.props;
if (Array.isArray(option.children) && option.children.length) {
@ -74,6 +77,7 @@ export class ListMenu extends React.Component<ListMenuProps> {
)}
key={index}
onClick={onSelect ? (e: any) => onSelect(e, option) : undefined}
{...testIdBuilder?.getChild(option.value || index).getTestId()}
{...getItemProps({
item: option,
index: index

View File

@ -10,7 +10,14 @@ import getMiniDecimal, {
} from '@rc-component/mini-decimal';
import {Icon} from './icons';
import {ThemeProps, themeable, isNumeric, autobind, ucFirst} from 'amis-core';
import {
ThemeProps,
themeable,
isNumeric,
autobind,
ucFirst,
TestIdBuilder
} from 'amis-core';
export type ValueType = string | number;
@ -75,6 +82,8 @@ export interface NumberProps extends ThemeProps {
*
*/
inputControlClassName?: string;
testIdBuilder?: TestIdBuilder;
}
export interface NumberState {
@ -319,7 +328,8 @@ export class NumberInput extends React.Component<NumberProps, NumberState> {
keyboard,
inputControlClassName,
mobileUI,
name
name,
testIdBuilder
} = this.props;
const precisionProps: any = {
precision: NumberInput.normalizePrecision(precision, step)
@ -359,6 +369,7 @@ export class NumberInput extends React.Component<NumberProps, NumberState> {
stringMode={this.isBig ? true : false}
keyboard={keyboard}
{...precisionProps}
{...testIdBuilder?.getTestId()}
/>
);
}

View File

@ -5,7 +5,12 @@
*/
import React from 'react';
import isInteger from 'lodash/isInteger';
import {localeable, LocaleProps, resolveEventData} from 'amis-core';
import {
localeable,
LocaleProps,
resolveEventData,
TestIdBuilder
} from 'amis-core';
import {themeable, ThemeProps} from 'amis-core';
import {autobind} from 'amis-core';
import {Icon} from './icons';
@ -120,6 +125,7 @@ export interface PaginationProps
ThemeProps,
LocaleProps {
popOverContainer?: any;
testIdBuilder?: TestIdBuilder;
}
export interface PaginationState {
pageNum: string;
@ -189,7 +195,7 @@ export class Pagination extends React.Component<
* @param page
*/
renderPageItem(page: number) {
const {classnames: cx, activePage} = this.props;
const {classnames: cx, activePage, testIdBuilder} = this.props;
const {perPage} = this.state;
return (
@ -200,7 +206,12 @@ export class Pagination extends React.Component<
'is-active': page === activePage
})}
>
<a role="button">{page}</a>
<a
role="button"
{...testIdBuilder?.getChild(`page-${page}`).getTestId()}
>
{page}
</a>
</li>
);
}
@ -212,7 +223,12 @@ export class Pagination extends React.Component<
* @param page
*/
renderEllipsis(key: string) {
const {classnames: cx, activePage, ellipsisPageGap} = this.props;
const {
classnames: cx,
activePage,
ellipsisPageGap,
testIdBuilder
} = this.props;
const {perPage} = this.state;
const lastPage = this.getLastPage();
const gap: number =
@ -240,6 +256,7 @@ export class Pagination extends React.Component<
isPrevEllipsis ? 'backward' : 'forward'
);
}}
{...testIdBuilder?.getChild(key).getTestId()}
>
<a role="button">...</a>
<span className="icon">{jumpContent}</span>
@ -382,7 +399,8 @@ export class Pagination extends React.Component<
popOverContainerSelector,
mobileUI,
size,
translate: __
translate: __,
testIdBuilder
} = this.props;
let maxButtons = this.props.maxButtons;
const {pageNum, perPage, internalPageNum} = this.state;
@ -402,6 +420,7 @@ export class Pagination extends React.Component<
onKeyUp={this.handleSimpleKeyUp}
onBlur={this.handleSimpleBlur}
value={internalPageNum}
{...testIdBuilder?.getChild('simple-input').getTestId()}
/>
/
<span className={cx('Pagination-simplego-right')} key="go-right">
@ -419,6 +438,7 @@ export class Pagination extends React.Component<
className
)}
style={style}
{...testIdBuilder?.getTestId()}
>
<ul
key="pager-items"
@ -445,7 +465,7 @@ export class Pagination extends React.Component<
}}
key="prev"
>
<span>
<span {...testIdBuilder?.getChild(`go-prev`).getTestId()}>
<Icon icon="left-arrow" className="icon" />
</span>
</li>
@ -466,7 +486,7 @@ export class Pagination extends React.Component<
}}
key="next"
>
<span>
<span {...testIdBuilder?.getChild(`go-next`).getTestId()}>
<Icon icon="right-arrow" className="icon" />
</span>
</li>
@ -557,7 +577,7 @@ export class Pagination extends React.Component<
}}
key="prev"
>
<span>
<span {...testIdBuilder?.getChild('go-prev').getTestId()}>
<Icon icon="left-arrow" className="icon" />
</span>
</li>
@ -576,7 +596,7 @@ export class Pagination extends React.Component<
}}
key="next"
>
<span>
<span {...testIdBuilder?.getChild('go-next').getTestId()}>
<Icon icon="right-arrow" className="icon" />
</span>
</li>
@ -613,6 +633,7 @@ export class Pagination extends React.Component<
this.handlePageNumChange(v, perPage);
}}
value={pageNum}
{...testIdBuilder?.getChild('go-input').getTestId()}
/>
<span
className={cx('Pagination-inputGroup-right')}
@ -624,6 +645,7 @@ export class Pagination extends React.Component<
this.setState({pageNum: ''});
this.handlePageNumChange(+pageNum, perPage);
}}
{...testIdBuilder?.getChild('go').getTestId()}
>
{__('Pagination.go')}
</span>
@ -649,6 +671,7 @@ export class Pagination extends React.Component<
});
this.handlePageNumChange(1, p.value);
}}
{...testIdBuilder?.getChild('perpage').getTestId()}
/>
);
// total或者lastpage不存在不渲染总数
@ -667,6 +690,7 @@ export class Pagination extends React.Component<
{disabled: disabled},
className
)}
{...testIdBuilder?.getTestId()}
>
{layoutList.map(layoutItem => {
if (layoutItem === PaginationWidget.Pager) {

View File

@ -22,6 +22,7 @@ import {value2array, OptionProps, Option} from './Select';
import chunk from 'lodash/chunk';
import {ClassNamesFn, themeable} from 'amis-core';
import {columnsSplit} from 'amis-core';
import {TestIdBuilder} from 'amis-core';
interface RadioProps extends OptionProps {
id?: string;
@ -42,6 +43,7 @@ interface RadioProps extends OptionProps {
classPrefix: string;
classnames: ClassNamesFn;
renderLabel?: (item: Option, props: RadioProps) => JSX.Element;
testIdBuilder?: TestIdBuilder;
}
const defaultLabelRender = (item: Option, props: RadioProps) => (
@ -123,8 +125,10 @@ export class Radios extends React.Component<RadioProps, any> {
level,
btnActiveLevel,
classPrefix: ns,
testIdBuilder,
renderLabel = defaultLabelRender
} = this.props;
const itemTestIdBuilder = testIdBuilder?.getChild(option.value || index);
if (optionType === 'button') {
const active = !!~valueArray.indexOf(option);
@ -153,6 +157,7 @@ export class Radios extends React.Component<RadioProps, any> {
description={option.description}
inline={inline}
labelClassName={labelClassName}
testIdBuilder={itemTestIdBuilder}
>
{renderLabel(option, this.props)}
</Checkbox>

View File

@ -1,4 +1,4 @@
import {ThemeProps, themeable} from 'amis-core';
import {TestIdBuilder, ThemeProps, themeable} from 'amis-core';
import React from 'react';
import omit from 'lodash/omit';
import isInteger from 'lodash/isInteger';
@ -30,6 +30,7 @@ export interface ResultBoxProps
showInvalidMatch?: boolean;
popOverContainer?: any;
showArrow?: boolean;
testIdBuilder?: TestIdBuilder;
}
export class ResultBox extends React.Component<ResultBoxProps> {
@ -116,7 +117,8 @@ export class ResultBox extends React.Component<ResultBoxProps> {
itemRender,
classnames: cx,
showInvalidMatch,
popOverContainer
popOverContainer,
testIdBuilder
} = this.props;
if (
@ -143,6 +145,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
{label: `+ ${tags.length - maxVisibleCount} ...`}
].map((item, index) => {
const isShowInvalid = showInvalidMatch && item?.__unmatched;
const itemTIB = testIdBuilder?.getChild(item.value || index);
return index === maxVisibleCount ? (
<TooltipWrapper
key={tags.length}
@ -162,11 +165,16 @@ export class ResultBox extends React.Component<ResultBoxProps> {
'is-invalid': showInvalidMatch && item?.__unmatched
})}
key={itemIndex}
{...itemTIB?.getTestId()}
>
<span className={cx('ResultBox-valueLabel')}>
{itemRender(item)}
</span>
<a data-index={itemIndex} onClick={this.removeItem}>
<a
data-index={itemIndex}
onClick={this.removeItem}
{...itemTIB?.getChild('close').getTestId()}
>
<Icon icon="close" className="icon" />
</a>
</div>
@ -197,11 +205,16 @@ export class ResultBox extends React.Component<ResultBoxProps> {
className={cx('ResultBox-value', {
'is-invalid': isShowInvalid
})}
{...itemTIB?.getTestId()}
>
<span className={cx('ResultBox-valueLabel')}>
{itemRender(item)}
</span>
<a data-index={index} onClick={this.removeItem}>
<a
data-index={index}
onClick={this.removeItem}
{...itemTIB?.getChild('close').getTestId()}
>
<Icon icon="close" className="icon" />
</a>
</div>
@ -210,26 +223,36 @@ export class ResultBox extends React.Component<ResultBoxProps> {
});
}
return tags.map((item, index) => (
<TooltipWrapper
container={popOverContainer}
placement={'top'}
tooltip={item['label']}
trigger={'hover'}
key={index}
>
<div
className={cx('ResultBox-value', {
'is-invalid': showInvalidMatch && item?.__unmatched
})}
return tags.map((item, index) => {
const itemTIB = testIdBuilder?.getChild(index);
return (
<TooltipWrapper
container={popOverContainer}
placement={'top'}
tooltip={item['label']}
trigger={'hover'}
key={index}
>
<span className={cx('ResultBox-valueLabel')}>{itemRender(item)}</span>
<a data-index={index} onClick={this.removeItem}>
<Icon icon="close" className="icon" />
</a>
</div>
</TooltipWrapper>
));
<div
className={cx('ResultBox-value', {
'is-invalid': showInvalidMatch && item?.__unmatched
})}
{...itemTIB?.getTestId()}
>
<span className={cx('ResultBox-valueLabel')}>
{itemRender(item)}
</span>
<a
data-index={index}
onClick={this.removeItem}
{...itemTIB?.getChild('close').getTestId()}
>
<Icon icon="close" className="icon" />
</a>
</div>
</TooltipWrapper>
);
});
}
render() {
@ -265,6 +288,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
overflowTagPopover,
showArrow,
popOverContainer,
testIdBuilder,
...rest
} = this.props;
const isFocused = this.state.isFocused;
@ -286,6 +310,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
onKeyPress={allowInput ? undefined : onKeyPress}
onFocus={allowInput ? undefined : onFocus}
onBlur={allowInput ? undefined : onBlur}
{...testIdBuilder?.getTestId()}
>
<div className={cx('ResultBox-value-wrap')}>
{Array.isArray(result) && result.length ? (
@ -320,6 +345,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
)}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
testIdBuilder={testIdBuilder?.getChild('input')}
/>
) : null}
@ -335,6 +361,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
className={cx('ResultBox-clear', {
'ResultBox-clear-with-arrow': hasDropDownArrow
})}
{...testIdBuilder?.getChild('clear').getTestId()}
>
<div className={cx('ResultBox-clear-wrap')}>
<Icon icon="input-clear" className="icon" />

View File

@ -15,6 +15,8 @@ import {LocaleProps, localeable, ClassNamesFn} from 'amis-core';
import TransferSearch from './TransferSearch';
import VirtualList, {AutoSizer} from './virtual-list';
import type {TestIdBuilder} from 'amis-core';
export interface ResultListProps extends ThemeProps, LocaleProps {
className?: string;
value?: Array<Option>;
@ -33,6 +35,7 @@ export interface ResultListProps extends ThemeProps, LocaleProps {
itemHeight?: number; // 每个选项的高度,主要用于虚拟渲染
virtualThreshold?: number; // 数据量多大的时候开启虚拟渲染
showInvalidMatch?: boolean;
testIdBuilder?: TestIdBuilder;
}
export interface ItemRenderStates {
@ -275,9 +278,10 @@ export class ResultList extends React.Component<
sortable,
labelField,
translate: __,
showInvalidMatch
showInvalidMatch,
testIdBuilder
} = this.props;
const itemTIB = testIdBuilder?.getChild(`item-${option.value || index}`);
return (
<div
style={styles}
@ -309,6 +313,7 @@ export class ResultList extends React.Component<
onClick={(e: React.MouseEvent<HTMLElement>) =>
this.handleCloseItem(e, option)
}
{...itemTIB?.getChild('close').getTestId()}
>
<Icon icon="close" className="icon" />
</a>

View File

@ -178,7 +178,8 @@ export class BaseResultTableSelection extends BaseSelection<
translate: __,
placeholder,
virtualThreshold,
itemHeight
itemHeight,
testIdBuilder
} = this.props;
const {searching, tableOptions, searchTableOptions} = this.state;
@ -208,6 +209,10 @@ export class BaseResultTableSelection extends BaseSelection<
rowIndex: number
) => {
const raw = cellRender(column, option, colIndex, rowIndex);
const itemTIB = testIdBuilder?.getChild(
`item-${option.value || rowIndex}`
);
if (colIndex === columns.length - 1) {
return (
<>
@ -219,6 +224,7 @@ export class BaseResultTableSelection extends BaseSelection<
e.stopPropagation();
this.handleCloseItem(option);
}}
{...itemTIB?.getChild(`close`).getTestId()}
>
<CloseIcon />
</span>

View File

@ -288,7 +288,8 @@ export class BaseResultTreeList extends React.Component<
placeholder,
virtualThreshold,
itemHeight,
loadingConfig
loadingConfig,
testIdBuilder
} = this.props;
const {treeOptions, searching, searchTreeOptions} = this.state;
@ -309,6 +310,7 @@ export class BaseResultTreeList extends React.Component<
onDelete={(option: Option) => this.deleteTreeChecked(option)}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
testIdBuilder={testIdBuilder}
/>
) : (
<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>

View File

@ -2,7 +2,7 @@ import React from 'react';
import isInteger from 'lodash/isInteger';
import debounce from 'lodash/debounce';
import moment from 'moment';
import {ThemeProps, themeable} from 'amis-core';
import {TestIdBuilder, ThemeProps, themeable} from 'amis-core';
import {Icon} from './icons';
import {uncontrollable} from 'amis-core';
import {autobind} from 'amis-core';
@ -56,6 +56,7 @@ export interface SearchBoxProps
history?: SearchHistoryOptions;
clearAndSubmit?: boolean;
loading?: boolean;
testIdBuilder?: TestIdBuilder;
}
export interface SearchBoxState {
@ -297,7 +298,8 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
mobileUI,
translate: __,
loading,
loadingConfig
loadingConfig,
testIdBuilder
} = this.props;
const {isFocused, inputValue} = this.state;
const {enable} = this.getHistoryOptions();
@ -315,6 +317,7 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
{'is-mobile': mobileUI}
)}
style={style}
{...testIdBuilder?.getTestId()}
>
<Input
name={name}
@ -327,10 +330,15 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
onBlur={this.handleBlur}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
testIdBuilder={testIdBuilder?.getChild('input')}
/>
{!mini && clearable && inputValue && !disabled ? (
<div className={cx('SearchBox-clearable')} onClick={this.handleClear}>
<div
className={cx('SearchBox-clearable')}
onClick={this.handleClear}
{...testIdBuilder?.getChild('clear').getTestId()}
>
<Icon icon="input-clear" className="icon" />
</div>
) : null}
@ -341,6 +349,7 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
'SearchBox-searchBtn--loading': loading
})}
onClick={this.handleSearch}
{...testIdBuilder?.getChild('search').getTestId()}
>
{loading ? (
<Spinner

View File

@ -9,8 +9,7 @@ import {
getOptionValue,
getOptionValueBindField,
labelToString,
uncontrollable,
buildTestId
uncontrollable
} from 'amis-core';
import React from 'react';
import isInteger from 'lodash/isInteger';
@ -41,7 +40,7 @@ import Checkbox from './Checkbox';
import Input from './Input';
import {LocaleProps, localeable} from 'amis-core';
import Spinner, {SpinnerExtraProps} from './Spinner';
import type {Option, Options} from 'amis-core';
import type {Option, Options, TestIdBuilder} from 'amis-core';
import {RemoteOptionsProps, withRemoteConfig} from './WithRemoteConfig';
import Picker from './Picker';
import PopUp from './PopUp';
@ -96,6 +95,7 @@ export interface OptionProps {
onEdit?: (value: Option, origin?: Option, skipForm?: boolean) => void;
removable?: boolean;
onDelete?: (value: Option) => void;
testIdBuilder?: TestIdBuilder;
}
export type OptionValue = string | number | null | undefined | Option;
@ -324,7 +324,6 @@ export interface SelectProps
LocaleProps,
SpinnerExtraProps {
className?: string;
testid?: string;
popoverClassName?: string;
showInvalidMatch?: boolean;
creatable: boolean;
@ -1016,7 +1015,8 @@ export class Select extends React.Component<SelectProps, SelectState> {
mobileUI,
filterOption = defaultFilterOption,
overlay,
loading
loading,
testIdBuilder
} = this.props;
const {selection} = this.state;
@ -1061,6 +1061,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
}
let label = labelToString(item[labelField]);
let optTestIdBudr = testIdBuilder?.getChild(`option-${label || index}`);
return (
<div
@ -1079,6 +1080,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
'is-highlight': highlightedIndex === index,
'is-active': checked
})}
{...optTestIdBudr?.getTestId()}
>
{renderMenu ? (
multiple ? (
@ -1089,6 +1091,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
this.handleChange(item);
}}
disabled={item.disabled}
testIdBuilder={optTestIdBudr?.getChild('chekbx')}
>
{renderMenu(item, {
multiple,
@ -1137,6 +1140,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
<span
className={cx('Select-option-content')}
title={typeof label === 'string' ? label : ''}
{...optTestIdBudr?.getChild('content').getTestId()}
>
{item.disabled
? label
@ -1322,7 +1326,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
borderMode,
mobileUI,
hasError,
testid,
testIdBuilder,
loadingConfig
} = this.props;
@ -1354,7 +1358,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
onClick={this.toggle}
onFocus={this.onFocus}
onBlur={this.onBlur}
{...buildTestId(testid)}
{...testIdBuilder?.getTestId()}
className={cx(
`Select`,
{
@ -1384,7 +1388,11 @@ export class Select extends React.Component<SelectProps, SelectState> {
(Array.isArray(value)
? value.length
: value != null && value !== resetValue) ? (
<a onClick={this.clearValue} className={cx('Select-clear')}>
<a
onClick={this.clearValue}
className={cx('Select-clear')}
{...testIdBuilder?.getChild('clear').getTestId()}
>
<Icon icon="input-clear" className="icon" />
</a>
) : null}
@ -1398,7 +1406,10 @@ export class Select extends React.Component<SelectProps, SelectState> {
/>
) : null}
<span className={cx('Select-arrow')}>
<span
className={cx('Select-arrow')}
{...testIdBuilder?.getChild('arrow').getTestId()}
>
<Icon icon="right-arrow-bold" className="icon" />
</span>
{isOpen ? this.renderOuter(options) : null}

View File

@ -23,6 +23,8 @@ import {
import Checkbox from './Checkbox';
import {Option, Options} from './Select';
import type {TestIdBuilder} from 'amis-core';
export interface BaseSelectionProps extends ThemeProps, LocaleProps {
options: Options;
className?: string;
@ -49,6 +51,7 @@ export interface BaseSelectionProps extends ThemeProps, LocaleProps {
placeholderRender?: (props: any) => JSX.Element | null;
checkAll?: boolean;
checkAllLabel?: string;
testIdBuilder?: TestIdBuilder;
}
export interface ItemRenderStates {

View File

@ -5,7 +5,7 @@
*/
import React from 'react';
import {ClassNamesFn, buildTestId, themeable} from 'amis-core';
import {ClassNamesFn, TestIdBuilder, themeable} from 'amis-core';
import {Spinner} from './Spinner';
const sizeMap = {
@ -44,7 +44,7 @@ interface SwitchProps {
root?: string;
show?: boolean;
};
testid?: string;
testIdBuilder?: TestIdBuilder;
}
export class Switch extends React.PureComponent<SwitchProps, any> {
@ -88,7 +88,7 @@ export class Switch extends React.PureComponent<SwitchProps, any> {
classnames: cx,
loading,
loadingConfig,
testid,
testIdBuilder,
...rest
} = this.props;
@ -112,7 +112,7 @@ export class Switch extends React.PureComponent<SwitchProps, any> {
'is-disabled': isDisabled
})}
data-role="switch"
{...buildTestId(testid)}
{...testIdBuilder?.getTestId()}
>
<input
type="checkbox"

View File

@ -77,7 +77,8 @@ export class TableSelection extends BaseSelection<TableSelectionProps, any> {
value,
disabled,
option2value,
multiple
multiple,
testIdBuilder
} = this.props;
let columns = this.getColumns();
let valueArray = BaseSelection.value2array(value, options, option2value);
@ -108,6 +109,7 @@ export class TableSelection extends BaseSelection<TableSelectionProps, any> {
onChange={this.toggleAll}
checked={partialChecked}
partial={partialChecked && !allChecked}
testIdBuilder={testIdBuilder?.getChild('check-all')}
/>
</th>
) : null}
@ -140,10 +142,12 @@ export class TableSelection extends BaseSelection<TableSelectionProps, any> {
multiple,
translate: __,
itemClassName,
resultMode
resultMode,
testIdBuilder
} = this.props;
const checked = valueArray.indexOf(option) !== -1;
const itemTIB = testIdBuilder?.getChild(`item-${option.value || rowIndex}`);
return (
<tr
@ -171,7 +175,12 @@ export class TableSelection extends BaseSelection<TableSelectionProps, any> {
this.toggleOption(option);
}}
>
<Checkbox size="sm" checked={checked} disabled={disabled} />
<Checkbox
size="sm"
checked={checked}
disabled={disabled}
testIdBuilder={itemTIB}
/>
</td>
) : null}
{columns.map((column, colIndex) => (

View File

@ -5,7 +5,13 @@
*/
import React from 'react';
import {ClassName, localeable, LocaleProps, Schema} from 'amis-core';
import {
ClassName,
localeable,
LocaleProps,
Schema,
TestIdBuilder
} from 'amis-core';
import Transition, {ENTERED, ENTERING} from 'react-transition-group/Transition';
import {themeable, ThemeProps, noop} from 'amis-core';
import {uncontrollable} from 'amis-core';
@ -59,6 +65,7 @@ export interface TabProps extends ThemeProps {
children?: React.ReactNode | Array<React.ReactNode>;
swipeable?: boolean;
onSelect?: (eventKey: string | number) => void;
testIdBuilder?: TestIdBuilder;
}
class TabComponent extends React.PureComponent<TabProps> {
@ -113,7 +120,8 @@ class TabComponent extends React.PureComponent<TabProps> {
children,
className,
swipeable,
mobileUI
mobileUI,
testIdBuilder
} = this.props;
return (
@ -140,6 +148,7 @@ class TabComponent extends React.PureComponent<TabProps> {
onTouchMove={swipeable && mobileUI ? this.onTouchMove : noop}
onTouchEnd={swipeable && mobileUI ? this.onTouchEnd : noop}
onTouchCancel={swipeable && mobileUI ? this.onTouchEnd : noop}
{...testIdBuilder?.getTestId()}
>
{children}
</div>
@ -181,6 +190,7 @@ export interface TabsProps extends ThemeProps, LocaleProps {
collapseBtnLabel?: string;
popOverContainer?: any;
children?: React.ReactNode | Array<React.ReactNode>;
testIdBuilder?: TestIdBuilder;
}
export interface IDragInfo {
@ -586,7 +596,8 @@ export class Tabs extends React.Component<TabsProps, any> {
draggable,
showTip,
showTipClassName,
editable
editable,
testIdBuilder
} = this.props;
const {
@ -646,7 +657,9 @@ export class Tabs extends React.Component<TabsProps, any> {
)}
</a>
);
const tabTestIdBuidr = testIdBuilder?.getChild(
`tab-${typeof title === 'string' ? title : index}`
);
return (
<li
className={cx(
@ -662,6 +675,7 @@ export class Tabs extends React.Component<TabsProps, any> {
typeof title === 'string' &&
this.handleStartEdit(index, title);
}}
{...tabTestIdBuidr?.getChild('link').getTestId()}
>
{showTip ? (
<TooltipWrapper
@ -684,6 +698,7 @@ export class Tabs extends React.Component<TabsProps, any> {
this.props.onClose &&
this.props.onClose(index, eventKey ?? index);
}}
{...tabTestIdBuidr?.getChild('close').getTestId()}
>
<Icon icon="close" className={cx('Tabs-link-close-icon')} />
</span>
@ -722,7 +737,7 @@ export class Tabs extends React.Component<TabsProps, any> {
}
renderArrow(type: 'left' | 'right') {
const {mode: dMode, tabsMode} = this.props;
const {mode: dMode, tabsMode, testIdBuilder} = this.props;
const mode = tabsMode || dMode;
if (['vertical', 'sidebar'].includes(mode)) {
return;
@ -738,6 +753,7 @@ export class Tabs extends React.Component<TabsProps, any> {
'Tabs-linksContainer-arrow--' + type,
disabled && 'Tabs-linksContainer-arrow--disabled'
)}
{...testIdBuilder?.getChild(`arrow-${type}`).getTestId()}
>
<Icon icon="right-arrow-bold" className="icon" />
</div>
@ -835,7 +851,8 @@ export class Tabs extends React.Component<TabsProps, any> {
draggable,
sidePosition,
addBtnText,
mobileUI
mobileUI,
testIdBuilder
} = this.props;
const {isOverflow} = this.state;
@ -851,6 +868,7 @@ export class Tabs extends React.Component<TabsProps, any> {
<div
className={cx('Tabs-addable')}
onClick={() => this.handleAddBtn()}
{...testIdBuilder?.getChild('add-tab').getTestId()}
>
<Icon icon="plus" className={cx('Tabs-addable-icon')} />
{addBtnText}
@ -871,6 +889,7 @@ export class Tabs extends React.Component<TabsProps, any> {
className
)}
style={style}
{...testIdBuilder?.getTestId()}
>
{!['vertical', 'sidebar', 'chrome'].includes(mode) ? (
<div
@ -885,6 +904,7 @@ export class Tabs extends React.Component<TabsProps, any> {
'Tabs-linksContainer',
isOverflow && 'Tabs-linksContainer--overflow'
)}
{...testIdBuilder?.getChild('links').getTestId()}
>
{!mobileUI ? this.renderArrow('left') : null}
<div className={cx('Tabs-linksContainer-main')}>
@ -911,6 +931,7 @@ export class Tabs extends React.Component<TabsProps, any> {
'is-mobile': mobileUI
})}
role="tablist"
{...testIdBuilder?.getChild('links').getTestId()}
>
{this.renderNavs()}
{additionBtns}
@ -925,7 +946,11 @@ export class Tabs extends React.Component<TabsProps, any> {
})}
</div>
{draggable && (
<div className={cx('Tabs-drag-tip')} ref={this.dragTipRef} />
<div
className={cx('Tabs-drag-tip')}
ref={this.dragTipRef}
{...testIdBuilder?.getChild('drag').getTestId()}
/>
)}
</div>
);

View File

@ -177,10 +177,12 @@ export class TabsTransfer extends React.Component<
activeKey,
options: optionsConfig,
valueField = 'value',
labelField = 'label'
labelField = 'label',
testIdBuilder
} = this.props;
const options = searchResult || [];
const mode = searchResultMode || selectMode; // 没有配置时默认和左侧选项展示形式一致
const searchTIB = testIdBuilder?.getChild('search-result');
const activeTab = optionsConfig[activeKey];
return mode === 'table' ? (
@ -196,6 +198,7 @@ export class TabsTransfer extends React.Component<
cellRender={cellRender}
itemHeight={itemHeight}
virtualThreshold={virtualThreshold}
testIdBuilder={searchTIB}
/>
) : mode === 'tree' ? (
<Tree
@ -221,6 +224,7 @@ export class TabsTransfer extends React.Component<
}
valueField={valueField}
labelField={labelField}
testIdBuilder={searchTIB}
/>
) : mode === 'chained' ? (
<ChainedCheckboxes
@ -243,6 +247,7 @@ export class TabsTransfer extends React.Component<
virtualThreshold={virtualThreshold}
valueField={valueField}
labelField={labelField}
testIdBuilder={searchTIB}
/>
) : (
<ListCheckboxes
@ -265,6 +270,7 @@ export class TabsTransfer extends React.Component<
virtualThreshold={virtualThreshold}
valueField={valueField}
labelField={labelField}
testIdBuilder={searchTIB}
/>
);
}
@ -279,7 +285,8 @@ export class TabsTransfer extends React.Component<
translate: __,
ctx,
mobileUI,
searchable
searchable,
testIdBuilder
} = this.props;
const showOptions = options.filter(item => item.visible !== false);
@ -297,6 +304,7 @@ export class TabsTransfer extends React.Component<
className={cx('TabsTransfer-tabs')}
onSelect={this.handleTabChange}
activeKey={activeKey}
testIdBuilder={testIdBuilder?.getChild('tabs')}
>
{showOptions.map((option, index) => (
<Tab
@ -307,6 +315,7 @@ export class TabsTransfer extends React.Component<
createObject(ctx, option)
)}
className="TabsTransfer-tab"
testIdBuilder={testIdBuilder?.getChild(`tab-${index}`)}
>
{option.searchable || searchable ? (
<div
@ -359,9 +368,11 @@ export class TabsTransfer extends React.Component<
initiallyOpen = true,
valueField = 'value',
labelField = 'label',
deferField = 'defer'
deferField = 'defer',
testIdBuilder
} = this.props;
const selectMode = option.selectMode || this.props.selectMode;
const selTIB = testIdBuilder?.getChild('selection');
return selectMode === 'table' ? (
<TableCheckboxes
@ -379,6 +390,7 @@ export class TabsTransfer extends React.Component<
virtualThreshold={virtualThreshold}
valueField={valueField}
labelField={labelField}
testIdBuilder={selTIB}
/>
) : selectMode === 'tree' ? (
<Tree
@ -409,6 +421,7 @@ export class TabsTransfer extends React.Component<
valueField={valueField}
labelField={labelField}
initiallyOpen={initiallyOpen}
testIdBuilder={selTIB}
/>
) : selectMode === 'chained' ? (
<ChainedCheckboxes
@ -434,6 +447,7 @@ export class TabsTransfer extends React.Component<
virtualThreshold={virtualThreshold}
valueField={valueField}
labelField={labelField}
testIdBuilder={selTIB}
/>
) : selectMode === 'associated' ? (
<AssociatedCheckboxes
@ -464,6 +478,7 @@ export class TabsTransfer extends React.Component<
valueField={valueField}
labelField={labelField}
deferField={deferField}
testIdBuilder={selTIB}
/>
) : (
<ListCheckboxes
@ -488,6 +503,7 @@ export class TabsTransfer extends React.Component<
virtualThreshold={virtualThreshold}
valueField={valueField}
labelField={labelField}
testIdBuilder={selTIB}
/>
);
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import {findDOMNode} from 'react-dom';
import BaseTextArea from 'react-textarea-autosize';
import {buildTestId, localeable, LocaleProps} from 'amis-core';
import {localeable, LocaleProps, TestIdBuilder} from 'amis-core';
import {themeable, ThemeProps} from 'amis-core';
import {autobind, ucFirst} from 'amis-core';
import {Icon} from './icons';
@ -58,7 +58,7 @@ export interface TextAreaProps extends ThemeProps, LocaleProps {
placeholder?: string;
name?: string;
disabled?: boolean;
testid?: string;
testIdBuilder?: TestIdBuilder;
forwardRef?: {current: HTMLTextAreaElement | null};
}
@ -185,7 +185,7 @@ export class Textarea extends React.Component<TextAreaProps, TextAreaState> {
maxLength,
showCounter,
clearable,
testid
testIdBuilder
} = this.props;
const counter = showCounter ? this.valueToString(value).length : 0;
@ -220,7 +220,7 @@ export class Textarea extends React.Component<TextAreaProps, TextAreaState> {
onChange={this.handleChange}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
{...buildTestId(testid)}
{...testIdBuilder?.getTestId()}
/>
{clearable && !disabled && value ? (

View File

@ -25,6 +25,8 @@ import ResultTreeList from './ResultTreeList';
import {SpinnerExtraProps} from './Spinner';
import Pagination from './Pagination';
import type {TestIdBuilder} from 'amis-core';
export type SelectMode =
| 'table'
| 'group'
@ -165,6 +167,7 @@ export interface TransferProps
* ui级联关系true代表级联选中false代表不级联true
*/
autoCheckChildren?: boolean;
testIdBuilder?: TestIdBuilder;
}
export interface TransferState {
@ -540,7 +543,8 @@ export class Transfer<
translate: __,
searchPlaceholder = __('Transfer.searchKeyword'),
mobileUI,
valueField = 'value'
valueField = 'value',
testIdBuilder
} = props;
if (selectRender) {
@ -585,6 +589,7 @@ export class Transfer<
partial={checkedPartial && !checkedAll}
onChange={props.onToggleAll || this.toggleAll}
size="sm"
testIdBuilder={testIdBuilder?.getChild('toggle-all')}
/>
) : null}
{__(selectTitle || 'Transfer.available')}
@ -605,6 +610,7 @@ export class Transfer<
'Transfer-checkAll',
disabled || !options.length ? 'is-disabled' : ''
)}
{...testIdBuilder?.getChild('toggle-all').getTestId()}
>
{__('Select.checkAll')}
</a>
@ -620,9 +626,13 @@ export class Transfer<
onKeyDown={this.handleSearchKeyDown}
placeholder={searchPlaceholder}
mobileUI={mobileUI}
testIdBuilder={testIdBuilder?.getChild('search-input')}
>
{this.state.searchResult !== null ? (
<a onClick={this.handleSeachCancel}>
<a
onClick={this.handleSeachCancel}
{...testIdBuilder?.getChild('search-cancel').getTestId()}
>
<Icon icon="close" className="icon" />
</a>
) : (
@ -702,7 +712,8 @@ export class Transfer<
virtualListHeight,
checkAll,
checkAllLabel,
onlyChildren
onlyChildren,
testIdBuilder
} = props;
const {isTreeDeferLoad, searchResult, inputValue} = this.state;
const options = searchResult ?? [];
@ -730,6 +741,7 @@ export class Transfer<
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
virtualListHeight={virtualListHeight}
testIdBuilder={testIdBuilder?.getChild('search-result')}
/>
) : mode === 'tree' ? (
<Tree
@ -793,6 +805,7 @@ export class Transfer<
virtualListHeight={virtualListHeight}
checkAllLabel={checkAllLabel}
checkAll={checkAll}
testIdBuilder={testIdBuilder?.getChild('search-result')}
/>
);
}
@ -826,7 +839,8 @@ export class Transfer<
checkAllLabel,
onlyChildren,
autoCheckChildren = true,
initiallyOpen = true
initiallyOpen = true,
testIdBuilder
} = props;
return selectMode === 'table' ? (
@ -846,6 +860,7 @@ export class Transfer<
virtualListHeight={virtualListHeight}
checkAllLabel={checkAllLabel}
checkAll={checkAll}
testIdBuilder={testIdBuilder?.getChild('selection')}
/>
) : selectMode === 'tree' ? (
<Tree
@ -871,6 +886,7 @@ export class Transfer<
checkAll={checkAll}
initiallyOpen={initiallyOpen}
autoCheckChildren={autoCheckChildren}
testIdBuilder={testIdBuilder?.getChild('selection')}
/>
) : selectMode === 'chained' ? (
<ChainedSelection
@ -891,6 +907,7 @@ export class Transfer<
loadingConfig={loadingConfig}
checkAllLabel={checkAllLabel}
checkAll={checkAll}
testIdBuilder={testIdBuilder?.getChild('selection')}
/>
) : selectMode === 'associated' ? (
<AssociatedSelection
@ -917,6 +934,7 @@ export class Transfer<
loadingConfig={loadingConfig}
checkAllLabel={checkAllLabel}
checkAll={checkAll}
testIdBuilder={testIdBuilder?.getChild('selection')}
/>
) : (
<GroupedSelection
@ -936,6 +954,7 @@ export class Transfer<
virtualListHeight={virtualListHeight}
checkAllLabel={checkAllLabel}
checkAll={checkAll}
testIdBuilder={testIdBuilder?.getChild('selection')}
/>
);
}
@ -962,7 +981,8 @@ export class Transfer<
loadingConfig,
showInvalidMatch,
pagination,
accumulatedOptions
accumulatedOptions,
testIdBuilder
} = this.props;
const {resultSelectMode, isTreeDeferLoad} = this.state;
const searchable = !isTreeDeferLoad && resultSearchable;
@ -987,6 +1007,7 @@ export class Transfer<
onSearch={onResultSearch}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
testIdBuilder={testIdBuilder?.getChild('result')}
/>
);
case 'tree':
@ -1008,6 +1029,7 @@ export class Transfer<
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
testIdBuilder={testIdBuilder?.getChild('result')}
/>
);
default:
@ -1028,6 +1050,7 @@ export class Transfer<
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
showInvalidMatch={showInvalidMatch}
testIdBuilder={testIdBuilder?.getChild('result')}
/>
);
}
@ -1050,7 +1073,8 @@ export class Transfer<
translate: __,
valueField = 'value',
mobileUI,
pagination
pagination,
testIdBuilder
} = this.props as any;
const {searchResult} = this.state;
@ -1113,6 +1137,7 @@ export class Transfer<
'Transfer-clearAll',
disabled || !this.valueArray.length ? 'is-disabled' : ''
)}
{...testIdBuilder?.getChild('clear-all').getTestId()}
>
{__('clear')}
</a>

View File

@ -27,7 +27,8 @@ import {
getTreeParent,
getTreeAncestors,
flattenTree,
flattenTreeWithLeafNodes
flattenTreeWithLeafNodes,
TestIdBuilder
} from 'amis-core';
import {Option, Options, value2array} from './Select';
import {themeable, ThemeProps, highlight} from 'amis-core';
@ -152,6 +153,8 @@ interface TreeSelectorProps extends ThemeProps, LocaleProps, SpinnerExtraProps {
// 全选按钮文案
checkAllLabel?: string;
enableDefaultIcon?: boolean;
testIdBuilder?: TestIdBuilder;
}
interface TreeSelectorState {
@ -766,7 +769,7 @@ export class TreeSelector extends React.Component<
});
}
renderInput(prfix: JSX.Element | null = null) {
renderInput(prfix: JSX.Element | null = null, testIdBuilder?: TestIdBuilder) {
const {classnames: cx, mobileUI, translate: __} = this.props;
const {inputValue} = this.state;
@ -786,11 +789,20 @@ export class TreeSelector extends React.Component<
onChange={this.handleInputChange}
value={inputValue}
placeholder={__('placeholder.enter')}
{...testIdBuilder?.getChild('input').getTestId()}
/>
<a data-tooltip={__('cancel')} onClick={this.handleCancel}>
<a
data-tooltip={__('cancel')}
onClick={this.handleCancel}
{...testIdBuilder?.getChild('cancel').getTestId()}
>
<Icon icon="close" className="icon" />
</a>
<a data-tooltip={__('confirm')} onClick={this.handleConfirm}>
<a
data-tooltip={__('confirm')}
onClick={this.handleConfirm}
{...testIdBuilder?.getChild('confirm').getTestId()}
>
<Icon icon="check" className="icon" />
</a>
</div>
@ -1127,7 +1139,8 @@ export class TreeSelector extends React.Component<
loadingConfig,
enableDefaultIcon,
valueField,
mobileUI
mobileUI,
testIdBuilder
} = this.props;
const item = this.state.flattenedOptions[index];
@ -1142,6 +1155,9 @@ export class TreeSelector extends React.Component<
const disabled = this.isItemDisabled(item, checked);
const partial = this.isItemChildrenPartialChecked(item, checked);
const checkedInValue = !!~this.state.value.indexOf(item);
const itemTestBuilder = testIdBuilder?.getChild(
`item-${item[valueField] || item[labelField] || index}`
);
const checkbox: JSX.Element | null = multiple ? (
<Checkbox
@ -1150,6 +1166,7 @@ export class TreeSelector extends React.Component<
checked={checked || partial}
partial={partial}
onChange={this.handleCheck.bind(this, item, !checked)}
testIdBuilder={itemTestBuilder?.getChild('chekbx')}
/>
) : showRadio ? (
<Checkbox
@ -1157,6 +1174,7 @@ export class TreeSelector extends React.Component<
disabled={disabled}
checked={checked}
onChange={this.handleSelect.bind(this, item)}
testIdBuilder={itemTestBuilder?.getChild('chekbx')}
/>
) : null;
@ -1174,12 +1192,14 @@ export class TreeSelector extends React.Component<
let body = null;
if (isEditing && editingItem === item) {
body = this.renderInput(checkbox);
body = this.renderInput(checkbox, itemTestBuilder?.getChild('edit'));
} else if (item.isAdding) {
body = this.renderInput(
<span className={cx('Tree-itemArrowPlaceholder')} />
<span className={cx('Tree-itemArrowPlaceholder')} />,
itemTestBuilder?.getChild('add')
);
} else {
const isFolded = !this.isUnfolded(item);
body = (
<div
className={cx('Tree-itemLabel', {
@ -1197,7 +1217,10 @@ export class TreeSelector extends React.Component<
onDragEnd={this.onDragEnd(item)}
>
{draggable && (
<a className={cx('Tree-itemDrager drag-bar')}>
<a
className={cx('Tree-itemDrager drag-bar')}
{...itemTestBuilder?.getChild('drag-bar').getTestId()}
>
<Icon icon="drag-bar" className="icon" />
</a>
)}
@ -1214,8 +1237,11 @@ export class TreeSelector extends React.Component<
<div
onClick={() => this.toggleUnfolded(item)}
className={cx('Tree-itemArrow', {
'is-folded': !this.isUnfolded(item)
'is-folded': isFolded
})}
{...itemTestBuilder
?.getChild(isFolded ? 'open' : 'fold')
.getTestId()}
>
<Icon icon="down-arrow-bold" className="icon" />
</div>
@ -1225,7 +1251,10 @@ export class TreeSelector extends React.Component<
{checkbox}
<div className={cx('Tree-itemLabel-item', {'is-mobile': mobileUI})}>
<div
className={cx('Tree-itemLabel-item', {'is-mobile': mobileUI})}
{...itemTestBuilder?.getChild('content').getTestId()}
>
{showIcon ? (
<i
className={cx(
@ -1263,6 +1292,7 @@ export class TreeSelector extends React.Component<
: this.handleSelect(item))
}
title={item[labelField]}
{...itemTestBuilder?.getChild('text').getTestId()}
>
{itemRender
? itemRender(item, {
@ -1291,7 +1321,10 @@ export class TreeSelector extends React.Component<
trigger={'hover'}
tooltipTheme="dark"
>
<a onClick={this.handleAdd.bind(this, item)}>
<a
onClick={this.handleAdd.bind(this, item)}
{...itemTestBuilder?.getChild('add').getTestId()}
>
<Icon icon="plus" className="icon" />
</a>
</TooltipWrapper>
@ -1304,7 +1337,10 @@ export class TreeSelector extends React.Component<
trigger={'hover'}
tooltipTheme="dark"
>
<a onClick={this.handleRemove.bind(this, item)}>
<a
onClick={this.handleRemove.bind(this, item)}
{...itemTestBuilder?.getChild('remove').getTestId()}
>
<Icon icon="minus" className="icon" />
</a>
</TooltipWrapper>
@ -1317,7 +1353,10 @@ export class TreeSelector extends React.Component<
trigger={'hover'}
tooltipTheme="dark"
>
<a onClick={this.handleEdit.bind(this, item)}>
<a
onClick={this.handleEdit.bind(this, item)}
{...itemTestBuilder?.getChild('edit').getTestId()}
>
<Icon icon="new-edit" className="icon" />
</a>
</TooltipWrapper>
@ -1340,6 +1379,7 @@ export class TreeSelector extends React.Component<
...style,
paddingLeft: `calc(${level} * var(--Tree-indent))`
}}
{...itemTestBuilder?.getTestId()}
>
{body}
</li>
@ -1473,7 +1513,8 @@ export class TreeSelector extends React.Component<
rootCreateTip,
disabled,
draggable,
translate: __
translate: __,
testIdBuilder
} = this.props;
const {
value,
@ -1493,6 +1534,7 @@ export class TreeSelector extends React.Component<
'is-disabled': isAdding || isEditing
})}
onClick={this.handleAdd.bind(this, null)}
{...testIdBuilder?.getChild('add').getTestId()}
>
<Icon icon="plus" className="icon" />
<span>{__(rootCreateTip)}</span>
@ -1508,6 +1550,7 @@ export class TreeSelector extends React.Component<
'is-draggable': draggable
})}
ref={this.root}
{...testIdBuilder?.getTestId()}
>
{(flattenedOptions && flattenedOptions.length) ||
addBtn ||
@ -1532,6 +1575,7 @@ export class TreeSelector extends React.Component<
<span
className={cx('Tree-itemText')}
onClick={this.clearSelect}
{...testIdBuilder?.getChild(`root-item`).getTestId()}
>
{showIcon ? (
<i className={cx('Tree-itemIcon Tree-rootIcon')}>
@ -1551,6 +1595,7 @@ export class TreeSelector extends React.Component<
onClick={this.handleAdd.bind(this, null)}
data-tooltip={rootCreateTip}
data-position="left"
{...testIdBuilder?.getChild(`root-add`).getTestId()}
>
<Icon icon="plus" className="icon" />
</a>

View File

@ -15,7 +15,7 @@ import {PickerOption} from '../PickerColumn';
import 'moment/locale/zh-cn';
import 'moment/locale/de';
import type {RendererEnv} from 'amis-core';
import type {RendererEnv, TestIdBuilder} from 'amis-core';
import type {unitOfTime} from 'moment';
/** 视图模式 */
@ -132,6 +132,7 @@ interface BaseDatePickerProps {
timeConstraints?: any;
timeRangeHeader?: string;
status?: ChangeEventViewStatus;
testIdBuilder?: TestIdBuilder;
}
interface BaseDatePickerState {
@ -456,7 +457,8 @@ class BaseDatePicker extends React.Component<
'mobileUI',
'showToolbar',
'embed',
'env'
'env',
'testIdBuilder'
].forEach(key => (props[key] = (this.props as any)[key]));
return props;
@ -721,8 +723,14 @@ class BaseDatePicker extends React.Component<
}
render() {
const {viewMode, timeFormat, dateFormat, timeRangeHeader, mobileUI} =
this.props;
const {
viewMode,
timeFormat,
dateFormat,
timeRangeHeader,
mobileUI,
testIdBuilder
} = this.props;
const Component = CustomCalendarContainer as any;
const viewProps = this.getComponentProps();
@ -757,6 +765,7 @@ class BaseDatePicker extends React.Component<
? 'rdtTime'
: ''
)}
{...testIdBuilder?.getTestId()}
>
<div
key="dt"

View File

@ -15,7 +15,7 @@ import {
ClassNamesFn,
convertArrayValueToMoment
} from 'amis-core';
import type {RendererEnv} from 'amis-core';
import type {RendererEnv, TestIdBuilder} from 'amis-core';
import Picker from '../Picker';
import {PickerOption} from '../PickerColumn';
import {DateType} from './Calendar';
@ -89,6 +89,7 @@ interface CustomDaysViewProps extends LocaleProps {
getColumns: (types: DateType[], dateBoundary: void) => any;
getDateBoundary: (currentDate: moment.Moment) => any;
timeConstraints?: any;
testIdBuilder?: TestIdBuilder;
}
export class CustomDaysView extends React.Component<CustomDaysViewProps> {
@ -422,7 +423,7 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
renderDay = (props: any, currentDate: moment.Moment) => {
const {todayActiveStyle} = props; /** 只有today才会传入这个属性 */
const {classnames: cx, translate: __, env} = this.props;
const {classnames: cx, translate: __, testIdBuilder} = this.props;
const injectedProps = omit(props, ['todayActiveStyle']);
/** 某些情况下需要用inline style覆盖动态class需要hack important的样式 */
const todayDomRef = (node: HTMLSpanElement | null) => {
@ -611,7 +612,11 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
return (
<td {...injectedProps}>
<span style={todayActiveStyle} ref={todayDomRef}>
<span
style={todayActiveStyle}
ref={todayDomRef}
{...testIdBuilder?.getChild(props.key)?.getTestId()}
>
{currentDate.date()}
</span>
</td>
@ -637,7 +642,8 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
selectedDate,
viewDate,
isEndDate,
classnames: cx
classnames: cx,
testIdBuilder
} = this.props;
const date = selectedDate || (isEndDate ? viewDate.endOf('day') : viewDate);
@ -685,6 +691,7 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
)
)
});
const itemTIB = testIdBuilder?.getChild(type);
return (
<div
className={cx(
@ -718,6 +725,7 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
? option.value === date.format(formatMap[type])
: option.value === options?.[0]?.value
})}
{...itemTIB?.getChild(option.value)?.getTestId()}
onClick={() => {
this.setTime(type, parseInt(option.value, 10));
this.scrollToTop(
@ -834,7 +842,8 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
mobileUI,
embed,
timeFormat,
classnames: cx
classnames: cx,
testIdBuilder
} = this.props;
const locale = date.localeData();
const __ = this.props.translate;
@ -851,6 +860,7 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
<a
className="rdtPrev"
onClick={this.props.subtractTime(1, 'years')}
{...testIdBuilder?.getChild('prev-year').getTestId()}
>
<Icon
icon="right-double-arrow"
@ -860,6 +870,7 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
<a
className="rdtPrev"
onClick={this.props.subtractTime(1, 'months')}
{...testIdBuilder?.getChild('prev-month').getTestId()}
>
<Icon
icon="right-arrow"
@ -871,12 +882,14 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
<a
className="rdtSwitch"
onClick={this.props.showView('years')}
{...testIdBuilder?.getChild('switch-years').getTestId()}
>
{date.format(__('dateformat.year'))}
</a>
<a
className="rdtSwitch"
onClick={this.props.showView('months')}
{...testIdBuilder?.getChild('switch-months').getTestId()}
>
{date.format(__('MMM'))}
</a>
@ -885,10 +898,15 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
<a
className="rdtNext"
onClick={this.props.addTime(1, 'months')}
{...testIdBuilder?.getChild('next-month').getTestId()}
>
<Icon icon="right-arrow" className="icon date-icon-arrow" />
</a>
<a className="rdtNext" onClick={this.props.addTime(1, 'years')}>
<a
className="rdtNext"
onClick={this.props.addTime(1, 'years')}
{...testIdBuilder?.getChild('next-year').getTestId()}
>
<Icon
icon="right-double-arrow"
className="icon date-icon-arrow"

View File

@ -4,6 +4,7 @@ import {LocaleProps, localeable, getRange} from 'amis-core';
import Picker from '../Picker';
import {PickerOption} from '../PickerColumn';
import {DateType} from './Calendar';
import type {TestIdBuilder} from 'amis-core';
export interface OtherProps {
inputFormat?: string;
@ -47,6 +48,7 @@ export interface CustomMonthsViewProps extends LocaleProps {
timeCell: (value: number, type: DateType) => string;
getDateBoundary: (currentDate: moment.Moment) => any;
mobileUI: boolean;
testIdBuilder?: TestIdBuilder;
}
export class CustomMonthsView extends React.Component<CustomMonthsViewProps> {
@ -71,6 +73,7 @@ export class CustomMonthsView extends React.Component<CustomMonthsViewProps> {
}
renderMonths() {
const {testIdBuilder} = this.props;
let date = this.props.selectedDate,
month = this.props.viewDate.month(),
year = this.props.viewDate.year(),
@ -159,7 +162,7 @@ export class CustomMonthsView extends React.Component<CustomMonthsViewProps> {
year: number,
date: moment.Moment
) => {
const {translate: __} = this.props;
const {translate: __, testIdBuilder} = this.props;
const {viewDate: localMoment, ...rest} = props;
const monthStr = localMoment.month(month).format(__('MMM'));
const strLength = 3;
@ -169,7 +172,9 @@ export class CustomMonthsView extends React.Component<CustomMonthsViewProps> {
return (
<td {...rest}>
<span>{monthStrFixedLength}</span>
<span {...testIdBuilder?.getChild(props.key).getTestId()}>
{monthStrFixedLength}
</span>
</td>
);
};
@ -248,6 +253,7 @@ export class CustomMonthsView extends React.Component<CustomMonthsViewProps> {
render() {
const __ = this.props.translate;
const {testIdBuilder} = this.props;
const showYearHead =
!/^mm$/i.test(this.props.inputFormat || '') && !this.props.hideHeader;
const canClick = /yy/i.test(this.props.inputFormat || '');
@ -264,6 +270,7 @@ export class CustomMonthsView extends React.Component<CustomMonthsViewProps> {
<th
className="rdtPrev"
onClick={this.props.subtractTime(1, 'years')}
{...testIdBuilder?.getChild('prev-year').getTestId()}
>
&laquo;
</th>
@ -271,6 +278,7 @@ export class CustomMonthsView extends React.Component<CustomMonthsViewProps> {
<th
className="rdtSwitch"
onClick={this.props.showView('years')}
{...testIdBuilder?.getChild('switch-year').getTestId()}
>
{this.props.viewDate.format(__('dateformat.year'))}
</th>
@ -283,6 +291,7 @@ export class CustomMonthsView extends React.Component<CustomMonthsViewProps> {
<th
className="rdtNext"
onClick={this.props.addTime(1, 'years')}
{...testIdBuilder?.getChild('next-year').getTestId()}
>
&raquo;
</th>

View File

@ -5,6 +5,7 @@ import Picker from '../Picker';
import {PickerColumnItem} from '../PickerColumn';
import {getRange} from 'amis-core';
import {autobind} from 'amis-core';
import type {TestIdBuilder} from 'amis-core';
export interface QuarterViewProps extends LocaleProps, ThemeProps {
viewDate: moment.Moment;
@ -29,6 +30,7 @@ export interface QuarterViewProps extends LocaleProps, ThemeProps {
hideHeader?: boolean;
onConfirm?: (value: number[], types?: string[]) => void;
onClose?: () => void;
testIdBuilder?: TestIdBuilder;
}
export class QuarterView extends React.Component<QuarterViewProps> {
@ -38,7 +40,7 @@ export class QuarterView extends React.Component<QuarterViewProps> {
};
renderYear() {
const __ = this.props.translate;
const {translate: __, testIdBuilder} = this.props;
const showYearHead = !/^mm$/i.test(this.props.inputFormat || '');
if (!showYearHead) {
@ -54,11 +56,16 @@ export class QuarterView extends React.Component<QuarterViewProps> {
<th
className="rdtPrev"
onClick={this.props.subtractTime(1, 'years')}
{...testIdBuilder?.getChild('prev-year').getTestId()}
>
&laquo;
</th>
{canClick ? (
<th className="rdtSwitch" onClick={this.props.showView('years')}>
<th
className="rdtSwitch"
onClick={this.props.showView('years')}
{...testIdBuilder?.getChild('switch-year').getTestId()}
>
{this.props.viewDate.format(__('dateformat.year'))}
</th>
) : (
@ -67,7 +74,11 @@ export class QuarterView extends React.Component<QuarterViewProps> {
</th>
)}
<th className="rdtNext" onClick={this.props.addTime(1, 'years')}>
<th
className="rdtNext"
onClick={this.props.addTime(1, 'years')}
{...testIdBuilder?.getChild('next-year').getTestId()}
>
&raquo;
</th>
</tr>
@ -135,9 +146,12 @@ export class QuarterView extends React.Component<QuarterViewProps> {
year: number,
date: moment.Moment
) => {
const {testIdBuilder} = this.props;
return (
<td {...props}>
<span>Q{quartar}</span>
<span {...testIdBuilder?.getChild(props.key).getTestId()}>
Q{quartar}
</span>
</td>
);
};

View File

@ -14,6 +14,7 @@ import {PickerColumnItem} from '../PickerColumn';
import Downshift from 'downshift';
import type {Moment} from 'moment';
import type {TestIdBuilder} from 'amis-core';
interface CustomTimeViewProps extends LocaleProps {
viewDate: moment.Moment;
@ -52,6 +53,7 @@ interface CustomTimeViewProps extends LocaleProps {
onChange: (value: moment.Moment) => void;
timeConstraints?: any;
timeRangeHeader?: string;
testIdBuilder?: TestIdBuilder;
}
interface CustomTimeViewState {
@ -689,7 +691,8 @@ export class CustomTimeView extends React.Component<
isEndDate,
classnames: cx,
timeRangeHeader,
mobileUI
mobileUI,
testIdBuilder
} = this.props;
const __ = this.props.translate;
@ -745,6 +748,7 @@ export class CustomTimeView extends React.Component<
)
)
});
const itemTIB = testIdBuilder?.getChild(type);
return (
<div className={cx('CalendarInputWrapper')}>
<div
@ -767,6 +771,7 @@ export class CustomTimeView extends React.Component<
: option.value === options?.[0]?.value &&
!mobileUI
})}
{...itemTIB?.getChild(option.value).getTestId()}
onClick={() => {
this.setTime(type, parseInt(option.value, 10));
this.scrollToTop(
@ -793,7 +798,11 @@ export class CustomTimeView extends React.Component<
inputs.length && inputs.pop();
const quickLists = [
<a key="select-now" onClick={this.selectNowTime}>
<a
key="select-now"
onClick={this.selectNowTime}
{...testIdBuilder?.getChild('select-now').getTestId()}
>
{__('TimeNow')}
</a>
];
@ -809,6 +818,7 @@ export class CustomTimeView extends React.Component<
<a
className={cx('Button', 'Button--primary', 'Button--size-sm')}
onClick={this.confirm}
{...testIdBuilder?.getChild('confirm').getTestId()}
>
{__('confirm')}
</a>

View File

@ -2,6 +2,7 @@ import moment from 'moment';
import React from 'react';
import {LocaleProps, localeable, utils, getRange} from 'amis-core';
import Picker from '../Picker';
import type {TestIdBuilder} from 'amis-core';
interface CustomYearsViewProps extends LocaleProps {
viewDate: moment.Moment;
@ -31,6 +32,7 @@ interface CustomYearsViewProps extends LocaleProps {
currentDate: moment.Moment,
selected?: moment.Moment
) => boolean;
testIdBuilder?: TestIdBuilder;
}
export class CustomYearsView extends React.Component<CustomYearsViewProps> {
@ -124,9 +126,10 @@ export class CustomYearsView extends React.Component<CustomYearsViewProps> {
}
renderYear = (props: any, year: number, date?: moment.Moment) => {
const {testIdBuilder} = this.props;
return (
<td {...props}>
<span>{year}</span>
<span {...testIdBuilder?.getChild(props.key).getTestId()}>{year}</span>
</td>
);
};
@ -179,7 +182,7 @@ export class CustomYearsView extends React.Component<CustomYearsViewProps> {
render() {
let year = this.props.viewDate.year();
year = year - (year % 10);
const __ = this.props.translate;
const {testIdBuilder, translate: __} = this.props;
if (this.props.mobileUI) {
return <div className="rdtYears">{this.renderYearPicker()}</div>;
}
@ -191,13 +194,18 @@ export class CustomYearsView extends React.Component<CustomYearsViewProps> {
<th
className="rdtPrev"
onClick={this.props.subtractTime(10, 'years')}
{...testIdBuilder?.getChild('prev-year').getTestId()}
>
&laquo;
</th>
<th className="rdtSwitch">
{__('year-to-year', {from: year, to: year + 9})}
</th>
<th className="rdtNext" onClick={this.props.addTime(10, 'years')}>
<th
className="rdtNext"
onClick={this.props.addTime(10, 'years')}
{...testIdBuilder?.getChild('next-year').getTestId()}
>
&raquo;
</th>
</tr>

View File

@ -7,7 +7,13 @@
import React from 'react';
import pick from 'lodash/pick';
import {Item as RcItem, MenuItemProps as RcMenuItemProps} from 'rc-menu';
import {ClassNamesFn, themeable, createObject, buildTestId} from 'amis-core';
import {
ClassNamesFn,
themeable,
createObject,
TestIdBuilder,
filter
} from 'amis-core';
import {Badge} from '../Badge';
import {getIcon} from '../icons';
@ -30,6 +36,7 @@ export interface MenuItemProps
tooltipTrigger?: Trigger | Array<Trigger>;
renderLink: Function;
testid?: string;
testIdBuilder?: TestIdBuilder;
extra?: React.ReactNode;
}
@ -91,7 +98,7 @@ export class MenuItem extends React.Component<MenuItemProps> {
renderLink,
extra,
disabled,
testid,
testIdBuilder,
id,
data: defaultData
} = this.props;
@ -168,7 +175,7 @@ export class MenuItem extends React.Component<MenuItemProps> {
data-id={link?.__id || id}
data-depth={depth}
onDragStart={onDragStart?.(link)}
{...buildTestId(testid, link)}
{...testIdBuilder?.getTestId()}
>
{isCollapsedNode ? (
<>{iconNode || labelNode}</>

View File

@ -12,8 +12,8 @@ import {
themeable,
autobind,
createObject,
filter,
buildTestId
TestIdBuilder,
filter
} from 'amis-core';
import {getIcon, Icon} from '../icons';
@ -42,6 +42,7 @@ export interface SubMenuProps
onTitleClick?: (e: MenuItemTitleInfo) => void;
renderLink: Function;
[propName: string]: any;
testIdBuilder?: TestIdBuilder;
}
export class SubMenu extends React.Component<SubMenuProps> {
@ -111,7 +112,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
disabled,
data: defaultData,
extra,
testid,
testIdBuilder,
renderLink
} = this.props;
const isCollapsedNode = collapsed && depth === 1;
@ -166,7 +167,11 @@ export class SubMenu extends React.Component<SubMenuProps> {
) : null;
const dragNode =
!disabled && stacked && mode === 'inline' && !collapsed && draggable ? (
<span className={cx('Nav-Menu-item-dragBar')} draggable>
<span
className={cx('Nav-Menu-item-dragBar')}
draggable
{...testIdBuilder?.getChild('drag-bar').getTestId()}
>
<DragIcon />
</span>
) : null;
@ -181,7 +186,11 @@ export class SubMenu extends React.Component<SubMenuProps> {
{labelNode}
{labelExtra}
{!stacked && depth === 1 ? (
<span key="expand-toggle" className={cx('Nav-Menu-submenu-arrow')}>
<span
key="expand-toggle"
className={cx('Nav-Menu-submenu-arrow')}
{...testIdBuilder?.getChild('expand-toggle').getTestId()}
>
<Icon icon="right-arrow-bold" className="icon" />
</span>
) : null}
@ -205,7 +214,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
data-id={link?.__id || id}
data-depth={depth}
onDragStart={onDragStart?.(link)}
{...buildTestId(testid, link)}
{...testIdBuilder?.getTestId()}
>
{renderContent()}
</a>

View File

@ -18,7 +18,8 @@ import {
autobind,
filterTree,
findTree,
getTreeAncestors
getTreeAncestors,
TestIdBuilder
} from 'amis-core';
import {ClassNamesFn, themeable} from 'amis-core';
@ -63,7 +64,7 @@ export interface MenuProps extends Omit<RcMenuProps, 'mode'> {
*/
navigations: Array<NavigationItem>;
testid?: string;
testIdBuilder?: TestIdBuilder;
/**
* stacked为true垂直 false
@ -542,8 +543,9 @@ export class Menu extends React.Component<MenuProps, MenuState> {
disabled?: boolean;
[propName: string]: any;
}) {
const {classnames: cx, expandIcon} = this.props;
const navigations = this.state.navigations;
const {classnames: cx, expandIcon, testIdBuilder} = this.props;
const link = findTree(navigations, item => item.id === ctx.eventKey);
return (
<span
key="expand-toggle"
@ -552,6 +554,10 @@ export class Menu extends React.Component<MenuProps, MenuState> {
this.handleToggleExpand(ctx);
e.preventDefault();
}}
{...testIdBuilder
?.getChild(link?.link?.testid || ctx.eventKey)
.getChild('expand-toggle')
.getTestId()}
>
{!React.isValidElement(expandIcon) ? (
<Icon
@ -580,7 +586,7 @@ export class Menu extends React.Component<MenuProps, MenuState> {
overflowedIndicator,
overflowMaxCount,
popupClassName,
testid
testIdBuilder
} = this.props;
return list.map((item: NavigationItem, index: number) => {
@ -616,7 +622,7 @@ export class Menu extends React.Component<MenuProps, MenuState> {
badge={badge}
renderLink={renderLink}
depth={level || 1}
testid={testid}
testIdBuilder={testIdBuilder?.getChild(link.testid || index)}
popupClassName={popupClassName}
>
{this.renderMenuContent(item.children || [], item.depth + 1)}
@ -638,7 +644,7 @@ export class Menu extends React.Component<MenuProps, MenuState> {
renderLink={renderLink}
badge={badge}
data={data}
testid={testid}
testIdBuilder={testIdBuilder?.getChild(link.testid || index)}
depth={level || 1}
order={index}
overflowedIndicator={overflowedIndicator}

View File

@ -130,7 +130,7 @@ import {
SchemaClassName,
SchemaExpression
} from 'amis-core';
import type {FormSchemaBase} from 'amis-core';
import type {FormSchemaBase, TestIdBuilder} from 'amis-core';
import {MultilineTextSchema} from './renderers/MultilineText';
import {DateRangeSchema} from './renderers/DateRange';
import {PasswordSchema} from './renderers/Password';
@ -726,6 +726,8 @@ export type SchemaFunction = string | Function;
export interface BaseSchema extends BaseSchemaWithoutType {
type: SchemaType;
testid?: string;
}
export interface Option {

View File

@ -10,8 +10,7 @@ import {
RendererProps,
ScopedContext,
uuid,
setThemeClassName,
getTestId
setThemeClassName
} from 'amis-core';
import {filter} from 'amis-core';
import {BadgeObject, Button, SpinnerExtraProps} from 'amis-ui';
@ -24,8 +23,6 @@ export interface ButtonSchema extends BaseSchema {
*/
id?: string;
testid?: string;
/**
*
*/
@ -749,7 +746,7 @@ export class Action extends React.Component<ActionProps, ActionState> {
wrapperCustomStyle,
css,
id,
testid,
testIdBuilder,
env
} = this.props;
@ -849,7 +846,7 @@ export class Action extends React.Component<ActionProps, ActionState> {
[activeClassName || 'is-active']: isActive
}
)}
testid={getTestId(testid, data)}
testIdBuilder={testIdBuilder}
style={style}
size={size}
level={

View File

@ -7,6 +7,7 @@ import {BaseSchema, SchemaIcon, SchemaUrlPath} from '../Schema';
import {filter} from 'amis-core';
import {resolveVariableAndFilter} from 'amis-core';
import {Breadcrumb} from 'amis-ui';
import type {TestIdBuilder} from 'amis-core';
export type BreadcrumbBaseItemSchema = {
/**
@ -101,6 +102,8 @@ export interface BreadcrumbSchema extends BaseSchema {
*
*/
tooltipPosition?: TooltipPositionType;
testIdBuilder?: TestIdBuilder;
}
export interface BreadcrumbProps

View File

@ -7,8 +7,7 @@ import {
RendererProps,
evalExpressionWithConditionBuilder,
filterTarget,
mapTree,
buildTestId
mapTree
} from 'amis-core';
import {SchemaNode, Schema, ActionObject, PlainObject} from 'amis-core';
import {CRUDStore, ICRUDStore} from 'amis-core';
@ -1990,7 +1989,8 @@ export default class CRUD extends React.Component<CRUDProps, any> {
render,
classnames: cx,
alwaysShowPagination,
perPageAvailable
perPageAvailable,
testIdBuilder
} = this.props;
const {page, lastPage} = store;
@ -2038,7 +2038,8 @@ export default class CRUD extends React.Component<CRUDProps, any> {
{render(
'pagination',
{
type: 'pagination'
type: 'pagination',
testIdBuilder: testIdBuilder?.getChild('pagination')
},
{
...extraProps,
@ -2085,7 +2086,8 @@ export default class CRUD extends React.Component<CRUDProps, any> {
perPageAvailable,
classnames: cx,
classPrefix: ns,
translate: __
translate: __,
testIdBuilder
} = this.props;
const items = childProps.items;
@ -2116,13 +2118,20 @@ export default class CRUD extends React.Component<CRUDProps, any> {
onChange={(value: any) => this.handleChangePage(1, value.value)}
clearable={false}
popOverContainer={this.parentContainer}
testIdBuilder={testIdBuilder?.getChild('perPage')}
/>
</div>
);
}
renderLoadMore() {
const {store, classPrefix: ns, classnames: cx, translate: __} = this.props;
const {
store,
classPrefix: ns,
classnames: cx,
translate: __,
testIdBuilder
} = this.props;
const {page, lastPage} = store;
return (
@ -2135,6 +2144,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
this.search({page: page + 1, loadDataMode: 'load-more'})
}
size="sm"
{...testIdBuilder?.getChild('loadMore').getTestId()}
>
{__('CRUD.loadMore')}
</Button>
@ -2213,7 +2223,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
return null;
}
const {render, store, mobileUI, translate: __} = this.props;
const {render, store, mobileUI, translate: __, testIdBuilder} = this.props;
const type = (toolbar as Schema).type || toolbar;
if (type === 'bulkActions' || type === 'bulk-actions') {
@ -2258,7 +2268,11 @@ export default class CRUD extends React.Component<CRUDProps, any> {
const cx = this.props.classnames;
if (len) {
return (
<div className={cx('Crud-toolbar')} key={index}>
<div
className={cx('Crud-toolbar')}
key={index}
{...testIdBuilder?.getChild('toolbar').getTestId()}
>
{children.map(({toolbar, dom: child}, index) => {
const type = (toolbar as Schema).type || toolbar;
let align =
@ -2525,7 +2539,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
onSearchableFromInit,
headerToolbarRender,
footerToolbarRender,
testid,
testIdBuilder,
...rest
} = this.props;
@ -2536,7 +2550,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
'is-mobile': isMobile()
})}
style={style}
{...buildTestId(testid)}
{...testIdBuilder?.getChild('wrapper').getTestId()}
>
{filter && (!store.filterTogggable || store.filterVisible)
? render(
@ -2547,7 +2561,8 @@ export default class CRUD extends React.Component<CRUDProps, any> {
submitText: __('search'),
...filter,
type: 'form',
api: null
api: null,
testIdBuilder: testIdBuilder?.getChild('filter')
},
{
key: 'filter',

View File

@ -30,8 +30,7 @@ import {
isApiOutdated,
isPureVariable,
resolveVariableAndFilter,
parsePrimitiveQueryString,
buildTestId
parsePrimitiveQueryString
} from 'amis-core';
import {Html, SpinnerExtraProps} from 'amis-ui';
import {
@ -1324,7 +1323,6 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
'is-loading': store.loading
})}
style={style}
{...buildTestId(testid)}
>
<div className={cx('Crud2-filter')}>
{this.renderFilter(filterSchema)}

View File

@ -459,6 +459,7 @@ export class CardRenderer extends React.Component<CardProps> {
level: 'link',
type: 'button',
...action,
testid: action.testid ? filter(action.testid, data) : index,
size
},
{

View File

@ -8,8 +8,7 @@ import {
isPureVariable,
resolveVariableAndFilter,
CustomStyle,
setThemeClassName,
buildTestId
setThemeClassName
} from 'amis-core';
import {DndContainer as DndWrapper} from 'amis-ui';
import {BaseSchema, SchemaClassName, SchemaCollection} from '../Schema';
@ -233,7 +232,6 @@ export default class Container<T> extends React.Component<
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={buildStyle(style, data)}
{...buildTestId(testid)}
>
{this.renderBody()}
<CustomStyle

View File

@ -7,8 +7,7 @@ import {
resolveVariableAndFilter,
setVariable,
setThemeClassName,
ValidateError,
getTestId
ValidateError
} from 'amis-core';
import {Renderer, RendererProps} from 'amis-core';
import {SchemaNode, Schema, ActionObject} from 'amis-core';
@ -54,8 +53,6 @@ export interface DialogSchema extends BaseSchema {
*/
actions?: Array<ActionSchema>;
testid?: string;
/**
*
*/
@ -246,7 +243,7 @@ export default class Dialog extends React.Component<DialogProps> {
}
buildActions(): Array<ActionSchema> {
const {actions, confirm, testid, translate: __} = this.props;
const {actions, confirm, translate: __, testIdBuilder} = this.props;
if (typeof actions !== 'undefined') {
return actions;
@ -255,7 +252,7 @@ export default class Dialog extends React.Component<DialogProps> {
let ret: Array<ActionSchema> = [];
ret.push({
type: 'button',
testid: getTestId(testid && `${testid}-cancel`),
testIdBuilder: testIdBuilder?.getChild('cancel'),
actionType: 'cancel',
label: __('cancel')
});
@ -263,7 +260,7 @@ export default class Dialog extends React.Component<DialogProps> {
if (confirm) {
ret.push({
type: 'button',
testid: getTestId(testid && `${testid}-confirm`),
testIdBuilder: testIdBuilder?.getChild('confirm'),
actionType: 'confirm',
label: __('confirm'),
primary: true

View File

@ -6,8 +6,7 @@ import {
isPureVariable,
resolveVariableAndFilter,
setThemeClassName,
ValidateError,
getTestId
ValidateError
} from 'amis-core';
import {Renderer, RendererProps} from 'amis-core';
import {SchemaNode, Schema, ActionObject} from 'amis-core';
@ -155,8 +154,6 @@ export interface DrawerSchema extends BaseSchema {
data?: {
[propName: string]: any;
};
testid?: string;
}
export type DrawerSchemaBase = Omit<DrawerSchema, 'type'>;
@ -272,7 +269,7 @@ export default class Drawer extends React.Component<DrawerProps> {
}
buildActions(): Array<ActionSchema> {
const {actions, confirm, testid, translate: __} = this.props;
const {actions, confirm, translate: __, testIdBuilder} = this.props;
if (typeof actions !== 'undefined') {
return actions;
@ -281,7 +278,7 @@ export default class Drawer extends React.Component<DrawerProps> {
let ret: Array<ActionSchema> = [];
ret.push({
type: 'button',
testid: getTestId(testid && `${testid}-cancel`),
testIdBuilder: testIdBuilder?.getChild('cancel'),
actionType: 'close',
label: __('cancel')
});
@ -290,7 +287,7 @@ export default class Drawer extends React.Component<DrawerProps> {
ret.push({
type: 'button',
actionType: 'confirm',
testid: getTestId(testid && `${testid}-confirm`),
testIdBuilder: testIdBuilder?.getChild('confirm'),
label: __('confirm'),
primary: true
});

View File

@ -4,7 +4,7 @@ import {Overlay} from 'amis-core';
import {PopOver} from 'amis-core';
import {TooltipWrapper} from 'amis-ui';
import {isDisabled, isVisible, noop, filterClassNameObject} from 'amis-core';
import {filter, buildTestId, getTestId} from 'amis-core';
import {filter} from 'amis-core';
import {Icon, hasIcon} from 'amis-ui';
import {
BaseSchema,
@ -52,8 +52,6 @@ export interface DropdownButtonSchema extends BaseSchema {
*/
buttons?: Array<DropdownButton>;
testid?: string;
/**
*
*/
@ -255,7 +253,13 @@ export default class DropDownButton extends React.Component<
button: DropdownButton,
index: number | string
): React.ReactNode {
const {render, classnames: cx, data, testid, ignoreConfirm} = this.props;
const {
render,
classnames: cx,
data,
ignoreConfirm,
testIdBuilder
} = this.props;
index = typeof index === 'number' ? index.toString() : index;
if (typeof button !== 'string' && Array.isArray(button?.children)) {
@ -302,11 +306,13 @@ export default class DropDownButton extends React.Component<
`button/${index}`,
{
type: 'button',
testid:
testid &&
`${getTestId(testid, data)}-${button.testid || index}`,
...(button as any),
className: ''
className: '',
// 防止dropdown中button没有 testid或者id
testIdBuilder: testIdBuilder?.getChild(
button.label || index,
data
)
},
{
isMenuItem: true,
@ -426,8 +432,8 @@ export default class DropDownButton extends React.Component<
trigger,
data,
hideCaret,
testid,
env
env,
testIdBuilder
} = this.props;
return (
@ -444,7 +450,6 @@ export default class DropDownButton extends React.Component<
className
)}
style={style}
{...buildTestId(testid, data)}
onMouseEnter={trigger === 'hover' ? this.open : () => {}}
onMouseLeave={trigger === 'hover' ? this.close : () => {}}
ref={this.domRef}
@ -459,6 +464,7 @@ export default class DropDownButton extends React.Component<
<button
onClick={this.toogle}
disabled={disabled || btnDisabled}
{...testIdBuilder?.getTestId(data)}
className={cx(
'Button',
btnClassName,

View File

@ -8,8 +8,7 @@ import {
Renderer,
RendererProps,
CustomStyle,
setThemeClassName,
buildTestId
setThemeClassName
} from 'amis-core';
import {Schema} from 'amis-core';
import {BaseSchema, SchemaCollection, SchemaObject} from '../Schema';
@ -152,7 +151,6 @@ export default class Flex extends React.Component<FlexProps, object> {
themeCss: wrapperCustomStyle
})
)}
{...buildTestId(testid)}
>
{(Array.isArray(items) ? items : items ? [items] : []).map(
(item, key) =>

View File

@ -4,7 +4,7 @@ import {
OptionsControlProps,
FormOptionsControl
} from 'amis-core';
import type {Option} from 'amis-core';
import {Option, TestIdBuilder} from 'amis-core';
import {ActionObject, isObject} from 'amis-core';
import type {BadgeObject} from 'amis-ui';
import {getLevelFromClassName, autobind, isEmpty} from 'amis-core';
@ -34,6 +34,7 @@ export interface ButtonGroupProps
| 'btnClassName'
> {
options: Array<Option>;
testIdBuilder: TestIdBuilder;
}
export default class ButtonGroupControl extends React.Component<
@ -102,6 +103,7 @@ export default class ButtonGroupControl extends React.Component<
vertical,
tiled,
badge,
testIdBuilder,
translate: __
} = props;
@ -137,6 +139,9 @@ export default class ButtonGroupControl extends React.Component<
active && 'ButtonGroup-button--active'
),
disabled: option.disabled || disabled,
testIdBuilder: testIdBuilder?.getChild(
`item-${option[labelField || 'label'] || key}`
),
onClick: (e: React.UIEvent<any>) => {
if (disabled) {
return;

View File

@ -319,6 +319,7 @@ export default class ChainedSelectControl extends React.Component<
multiple,
mobileUI,
env,
testIdBuilder,
...rest
} = this.props;
const arr = Array.isArray(value)
@ -341,6 +342,7 @@ export default class ChainedSelectControl extends React.Component<
}
classPrefix={ns}
key="base"
testIdBuilder={testIdBuilder?.getChild('base')}
options={Array.isArray(options) ? options : []}
value={arr[0]}
onChange={this.handleChange.bind(this, 0)}
@ -361,6 +363,7 @@ export default class ChainedSelectControl extends React.Component<
}
classPrefix={ns}
key={`x-${index + 1}`}
testIdBuilder={testIdBuilder?.getChild(`x-${index + 1}`)}
options={Array.isArray(options) ? options : []}
value={arr[index + 1]}
onChange={this.handleChange.bind(this, index + 1)}

View File

@ -12,6 +12,7 @@ import {autobind, createObject} from 'amis-core';
import {ActionObject} from 'amis-core';
import {BaseSchema, FormBaseControlSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
import type {TestIdBuilder} from 'amis-core';
export interface SchemaMap {
checkbox: CheckboxControlSchema;
@ -49,6 +50,7 @@ export interface CheckboxControlSchema extends FormBaseControlSchema {
partial?: boolean;
optionType?: 'default' | 'button';
checked?: boolean;
testIdBuilder?: TestIdBuilder;
}
export interface CheckboxProps
@ -139,6 +141,7 @@ export default class CheckboxControl extends React.Component<
optionType,
checked,
labelClassName,
testIdBuilder,
classPrefix: ns
} = this.props;
@ -155,6 +158,7 @@ export default class CheckboxControl extends React.Component<
optionType={optionType}
checked={checked}
labelClassName={labelClassName}
testIdBuilder={testIdBuilder}
>
{option ? render('option', option) : null}
</Checkbox>

View File

@ -12,6 +12,7 @@ import type {ActionObject, Api, OptionsControlProps, Option} from 'amis-core';
import {Checkbox, Icon, Spinner} from 'amis-ui';
import {FormOptionsSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
import type {TestIdBuilder} from 'amis-core';
/**
*
@ -43,6 +44,7 @@ export interface CheckboxesControlSchema extends FormOptionsSchema {
*
*/
menuTpl?: string;
testIdBuilder?: TestIdBuilder;
}
export interface CheckboxesProps
@ -258,10 +260,14 @@ export default class CheckboxesControl extends React.Component<
translate: __,
optionType,
menuTpl,
data
data,
testIdBuilder
} = this.props;
const labelText = String(option[labelField || 'label']);
const optionLabelClassName = option['labelClassName'];
const itemTestIdBuilder = testIdBuilder?.getChild(
'item-' + labelText || index
);
return (
<Checkbox
@ -274,6 +280,7 @@ export default class CheckboxesControl extends React.Component<
labelClassName={optionLabelClassName || labelClassName}
description={option.description}
optionType={optionType}
testIdBuilder={itemTestIdBuilder}
>
{menuTpl
? render(`checkboxes/${index}`, menuTpl, {

View File

@ -1,6 +1,8 @@
import React from 'react';
import {findDOMNode} from 'react-dom';
import cloneDeep from 'lodash/cloneDeep';
import isNumber from 'lodash/isNumber';
import get from 'lodash/get';
import {
FormItem,
FormControlProps,
@ -57,6 +59,8 @@ import type {SchemaTokenizeableString} from '../../Schema';
import isPlainObject from 'lodash/isPlainObject';
import isEqual from 'lodash/isEqual';
import type {TestIdBuilder} from 'amis-core';
export type ComboCondition = {
test: string;
items: Array<ComboSubControl>;
@ -75,6 +79,7 @@ export type ComboSubControl = SchemaObject & {
*
*/
columnClassName?: SchemaClassName;
testid?: string;
};
/**
@ -283,6 +288,7 @@ export interface ComboControlSchema extends FormBaseControlSchema {
maxLengthValidateFailed?: string;
};
updatePristineAfterStoreDataReInit?: boolean;
testIdBuilder?: TestIdBuilder;
}
export type ComboRendererEvent = 'add' | 'delete' | 'tabsChange';
@ -1395,9 +1401,10 @@ export default class ComboControl extends React.Component<ComboProps> {
removable,
deleteBtn,
mobileUI,
data
data,
testIdBuilder
} = this.props;
const delTestIdBuilder = testIdBuilder?.getChild(`delete-btn-${index}`);
const finnalRemovable =
store.removable !== false && // minLength ?
!disabled && // 控件自身是否禁用
@ -1424,6 +1431,7 @@ export default class ComboControl extends React.Component<ComboProps> {
'Combo-delController',
deleteBtn ? deleteBtn.className : ''
),
testIdBuilder: delTestIdBuilder,
onClick: (e: any) => {
if (!deleteBtn.onClick) {
this.deleteItem(index);
@ -1461,7 +1469,8 @@ export default class ComboControl extends React.Component<ComboProps> {
type: 'button',
className: cx('Combo-delController'),
label: deleteBtn,
onClick: this.deleteItem.bind(this, index)
onClick: this.deleteItem.bind(this, index),
testIdBuilder: delTestIdBuilder
});
}
@ -1473,6 +1482,7 @@ export default class ComboControl extends React.Component<ComboProps> {
className={cx(`Combo-delBtn ${!store.removable ? 'is-disabled' : ''}`)}
data-tooltip={!mobileUI ? __('delete') : null}
data-position="bottom"
{...delTestIdBuilder?.getTestId()}
>
{deleteIcon ? (
<i className={deleteIcon} />
@ -1500,10 +1510,12 @@ export default class ComboControl extends React.Component<ComboProps> {
addIcon,
conditions,
translate: __,
tabsMode
tabsMode,
testIdBuilder
} = this.props;
const hasConditions = Array.isArray(conditions) && conditions.length;
const addBtnTestIdBuilder = testIdBuilder?.getChild('add-button');
return (
<>
{store.addable &&
@ -1518,7 +1530,8 @@ export default class ComboControl extends React.Component<ComboProps> {
level: 'info',
size: 'sm',
closeOnClick: true,
btnClassName: addButtonClassName
btnClassName: addButtonClassName,
testIdBuilder: addBtnTestIdBuilder
},
{
buttons: conditions?.map(item => ({
@ -1539,12 +1552,14 @@ export default class ComboControl extends React.Component<ComboProps> {
render('add-button', {
...addBtn,
type: 'button',
testIdBuilder: addBtnTestIdBuilder,
onClick: () => this.addItem()
})
) : (
<Button
className={cx(`Combo-addBtn`, addButtonClassName)}
onClick={this.addItem}
testIdBuilder={addBtnTestIdBuilder}
>
{addIcon ? <Icon icon="plus-fine" className="icon" /> : null}
<span>{__(addButtonText || 'add')}</span>
@ -1804,8 +1819,19 @@ export default class ComboControl extends React.Component<ComboProps> {
lazyLoad,
translate: __,
static: isStatic,
testIdBuilder,
updatePristineAfterStoreDataReInit
} = this.props;
const finnalItems = Array.isArray(finnalControls)
? finnalControls.map((item, itemIndex) => {
const indexKey = index !== undefined && index >= 0 ? `-${index}` : '';
const key = `item-${item.testid || item.id || itemIndex}` + indexKey;
return {
...item,
testIdBuilder: testIdBuilder?.getChild(key)
};
})
: finnalControls;
// 单个
if (!multiple) {
@ -1813,7 +1839,7 @@ export default class ComboControl extends React.Component<ComboProps> {
'single',
{
type: 'form',
body: finnalControls,
body: finnalItems,
wrapperComponent: 'div',
wrapWithPanel: false,
mode: multiLine ? subFormMode || 'normal' : 'row',
@ -1840,7 +1866,7 @@ export default class ComboControl extends React.Component<ComboProps> {
`multiple/${index}`,
{
type: 'form',
body: finnalControls,
body: finnalItems,
wrapperComponent: 'div',
wrapWithPanel: false,
mode: tabsMode ? subFormMode : multiLine ? subFormMode : 'row',

View File

@ -16,6 +16,8 @@ import {localeable, LocaleProps} from 'amis-core';
import {FormBaseControlSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
import type {TestIdBuilder} from 'amis-core';
/**
* City
* https://aisuda.bce.baidu.com/amis/zh-CN/components/form/city
@ -86,6 +88,7 @@ export interface CityPickerProps
[propName: string]: any;
};
popOverContainer?: any;
testIdBuilder?: TestIdBuilder;
}
export interface CityDb {
@ -419,7 +422,8 @@ export class CityPicker extends React.Component<
translate: __,
loadingConfig,
popOverContainer,
itemClassName
itemClassName,
testIdBuilder
} = this.props;
const {provinceCode, cityCode, districtCode, street, db} = this.state;
@ -437,6 +441,7 @@ export class CityPicker extends React.Component<
value={provinceCode || ''}
onChange={this.handleProvinceChange}
popOverContainer={popOverContainer}
testIdBuilder={testIdBuilder?.getChild('province')}
/>
{allowCity && db.city[provinceCode] && db.city[provinceCode].length ? (
@ -451,6 +456,7 @@ export class CityPicker extends React.Component<
value={cityCode || ''}
onChange={this.handleCityChange}
popOverContainer={popOverContainer}
testIdBuilder={testIdBuilder?.getChild('city')}
/>
) : null}
@ -470,6 +476,7 @@ export class CityPicker extends React.Component<
value={districtCode || ''}
onChange={this.handleDistrictChange}
popOverContainer={popOverContainer}
testIdBuilder={testIdBuilder?.getChild('district')}
/>
) : null}
@ -481,6 +488,7 @@ export class CityPicker extends React.Component<
onBlur={this.handleStreetEnd}
placeholder={__('City.street')}
disabled={disabled}
{...testIdBuilder?.getChild('street').getTestId()}
/>
) : null}
</div>
@ -578,7 +586,8 @@ export class LocationControl extends React.Component<LocationControlProps> {
env,
mobileUI,
popOverContainer,
itemClassName
itemClassName,
testIdBuilder
} = this.props;
return mobileUI ? (
@ -607,6 +616,7 @@ export class LocationControl extends React.Component<LocationControlProps> {
joinValues={joinValues}
allowStreet={allowStreet}
disabled={disabled}
testIdBuilder={testIdBuilder}
/>
);
}

View File

@ -13,7 +13,7 @@ import {ActionObject} from 'amis-core';
import type {ShortCuts} from 'amis-ui/lib/components/DatePicker';
import {FormBaseControlSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
import type {TestIdBuilder} from 'amis-core';
/**
* DateRange
* https://aisuda.bce.baidu.com/amis/zh-CN/components/form/date-range
@ -134,6 +134,7 @@ export interface DateRangeProps
format: string;
valueFormat: string;
joinValues: boolean;
testIdBuilder?: TestIdBuilder;
}
export default class DateRangeControl extends React.Component<DateRangeProps> {

View File

@ -10,7 +10,8 @@ import {
autobind,
isObject,
resolveEventData,
dataMapping
dataMapping,
TestIdBuilder
} from 'amis-core';
import {FormBaseControlSchema, SchemaTokenizeableString} from '../../Schema';
import type {CellValue, CellRichTextValue} from 'exceljs';
@ -64,6 +65,8 @@ export interface InputExcelControlSchema extends FormBaseControlSchema {
autoFill?: {
[propName: string]: SchemaTokenizeableString;
};
testIdBuilder?: TestIdBuilder;
}
export interface ExcelProps
@ -401,7 +404,8 @@ export default class ExcelControl extends React.PureComponent<
classPrefix: ns,
disabled,
translate: __,
placeholder
placeholder,
testIdBuilder
} = this.props;
return (
@ -415,8 +419,14 @@ export default class ExcelControl extends React.PureComponent<
>
{({getRootProps, getInputProps}) => (
<section className={cx('ExcelControl-container', className)}>
<div {...getRootProps({className: cx('ExcelControl-dropzone')})}>
<input {...getInputProps()} />
<div
{...getRootProps({className: cx('ExcelControl-dropzone')})}
{...testIdBuilder?.getTestId()}
>
<input
{...getInputProps()}
{...testIdBuilder?.getChild('input').getTestId()}
/>
{this.state.filename ? (
__('Excel.parsed', {
filename: this.state.filename

View File

@ -3,8 +3,7 @@ import {
FormItem,
FormControlProps,
prettyBytes,
resolveEventData,
buildTestId
resolveEventData
} from 'amis-core';
import find from 'lodash/find';
import isPlainObject from 'lodash/isPlainObject';
@ -247,8 +246,6 @@ export interface FileControlSchema extends FormBaseControlSchema {
*
*/
drag?: boolean;
testid?: string;
}
export interface FileProps
@ -1358,9 +1355,9 @@ export default class FileControl extends React.Component<FileProps, FileState> {
data,
documentation,
documentLink,
testid,
env,
container
container,
testIdBuilder
} = this.props;
let {files, uploading, error} = this.state;
const nameField = this.props.nameField || 'name';
@ -1419,7 +1416,7 @@ export default class FileControl extends React.Component<FileProps, FileState> {
disabled={disabled}
{...getInputProps()}
capture={capture as any}
{...buildTestId(testid && `${testid}-input`)}
{...testIdBuilder?.getChild('input').getTestId()}
/>
{drag || isDragActive ? (
@ -1458,13 +1455,13 @@ export default class FileControl extends React.Component<FileProps, FileState> {
'is-disabled':
multiple && !!maxLength && files.length >= maxLength
})}
testid={testid}
tooltip={
multiple && maxLength && files.length >= maxLength
? __('File.maxLength', {maxLength})
: ''
}
onClick={this.handleSelect}
testIdBuilder={testIdBuilder?.getChild('select')}
>
<Icon icon="upload" className="icon" />
<span>
@ -1594,6 +1591,7 @@ export default class FileControl extends React.Component<FileProps, FileState> {
{!autoUpload && !hideUploadButton && files.length ? (
<Button
level="default"
testIdBuilder={testIdBuilder?.getChild('upload')}
disabled={!hasPending}
className={cx('FileControl-uploadBtn', btnUploadClassName)}
onClick={this.toggleUpload}

View File

@ -7,7 +7,8 @@ import {
resolveEventData,
CustomStyle,
formatInputThemeCss,
setThemeClassName
setThemeClassName,
TestIdBuilder
} from 'amis-core';
import cx from 'classnames';
import {NumberInput, Select} from 'amis-ui';
@ -158,6 +159,8 @@ export interface NumberProps extends FormControlProps {
*
*/
clearValueOnEmpty?: boolean;
testIdBuilder?: TestIdBuilder;
}
interface NumberState {
@ -446,7 +449,8 @@ export default class NumberControl extends React.Component<
inputControlClassName,
id,
env,
name
name,
testIdBuilder
} = this.props;
const {unit} = this.state;
const finalPrecision = this.filterNum(precision);
@ -525,6 +529,7 @@ export default class NumberControl extends React.Component<
displayMode={displayMode}
big={big}
clearValueOnEmpty={clearValueOnEmpty}
testIdBuilder={testIdBuilder}
/>
{Array.isArray(unitOptions) && unitOptions.length !== 0 ? (
unitOptions.length > 1 ? (

View File

@ -12,6 +12,7 @@ import {isMobile} from 'amis-core';
import {PopUp} from 'amis-ui';
import {autobind} from 'amis-core';
import type {TestIdBuilder} from 'amis-core';
/**
* SubForm
* https://aisuda.bce.baidu.com/amis/zh-CN/components/form/subform
@ -112,6 +113,7 @@ export interface SubFormProps extends FormControlProps {
minLength?: number;
maxLength?: number;
labelField?: string;
testIdBuilder?: TestIdBuilder;
}
export interface SubFormState {
@ -418,7 +420,8 @@ export default class SubFormControl extends React.PureComponent<
addable,
removable,
minLength,
addButtonText
addButtonText,
testIdBuilder
} = this.props;
return (
@ -435,6 +438,7 @@ export default class SubFormControl extends React.PureComponent<
itemClassName
)}
key={key}
{...testIdBuilder?.getChild(`item-${key}`).getTestId()}
>
{draggable && value.length > 1 ? (
<a className={cx('SubForm-valueDragBar')}>
@ -502,6 +506,7 @@ export default class SubFormControl extends React.PureComponent<
value.length >= maxLength
)
}
{...testIdBuilder?.getChild('add-button').getTestId()}
>
<Icon icon="plus" className="icon" />
<span>{__(addButtonText || 'SubForm.add')}</span>
@ -530,9 +535,12 @@ export default class SubFormControl extends React.PureComponent<
btnLabel,
render,
data,
translate: __
translate: __,
testIdBuilder
} = this.props;
const tIdBuilder = testIdBuilder?.getChild('edit-single');
return (
<div className={cx('SubForm-values', itemsClassName)} key="values">
<div
@ -546,6 +554,7 @@ export default class SubFormControl extends React.PureComponent<
onClick={this.editSingle}
data-tooltip={__('SubForm.editDetail')}
data-position="bottom"
{...tIdBuilder?.getTestId()}
>
<span className={cx('SubForm-valueLabel')}>
{btnLabel &&
@ -566,7 +575,10 @@ export default class SubFormControl extends React.PureComponent<
stripTag(value[labelField])) ||
__(defaultLabel))}
</span>
<a className={cx('SubForm-valueEdit')}>
<a
className={cx('SubForm-valueEdit')}
{...tIdBuilder?.getChild('icon').getTestId()}
>
<Icon icon="pencil" className="icon" />
</a>
</div>

View File

@ -980,7 +980,7 @@ export default class FormTable extends React.Component<TableProps, TableState> {
isCreateMode = false,
editRowIndex?: number
): Array<any> {
const {env, enableStaticTransform} = this.props;
const {env, enableStaticTransform, testIdBuilder} = this.props;
let columns: Array<any> = Array.isArray(props.columns)
? props.columns.concat()
: [];
@ -1016,6 +1016,9 @@ export default class FormTable extends React.Component<TableProps, TableState> {
tooltipContainer={props.popOverContainer || env.getModalContainer}
disabled={disabled}
onClick={this.addItem.bind(this, rowIndex + offset, undefined)}
testIdBuilder={testIdBuilder?.getChild(
`addRow-${rowIndex + offset}`
)}
>
{props.addBtnIcon ? (
<Icon
@ -1051,6 +1054,9 @@ export default class FormTable extends React.Component<TableProps, TableState> {
tooltipContainer={props.popOverContainer || env.getModalContainer}
disabled={disabled}
onClick={this.copyItem.bind(this, rowIndex + offset, undefined)}
testIdBuilder={testIdBuilder?.getChild(
`copyRow-${rowIndex + offset}`
)}
>
{props.copyBtnIcon ? (
<Icon
@ -1149,6 +1155,9 @@ export default class FormTable extends React.Component<TableProps, TableState> {
}
disabled={disabled}
onClick={() => this.editItem(rowIndex + offset)}
testIdBuilder={testIdBuilder?.getChild(
`editRow-${rowIndex + offset}`
)}
>
{/* 兼容之前的写法 */}
{typeof props.updateBtnIcon !== 'undefined' ? (
@ -1195,6 +1204,9 @@ export default class FormTable extends React.Component<TableProps, TableState> {
props.popOverContainer || env.getModalContainer
}
onClick={this.confirmEdit}
testIdBuilder={testIdBuilder?.getChild(
`confirmRow-${rowIndex + offset}`
)}
>
{props.confirmBtnIcon ? (
<Icon
@ -1232,6 +1244,9 @@ export default class FormTable extends React.Component<TableProps, TableState> {
props.popOverContainer || env.getModalContainer
}
onClick={this.cancelEdit}
testIdBuilder={testIdBuilder?.getChild(
`cancelRow-${rowIndex + offset}`
)}
>
{props.cancelBtnIcon ? (
<Icon
@ -1287,6 +1302,9 @@ export default class FormTable extends React.Component<TableProps, TableState> {
tooltipContainer={props.popOverContainer || env.getModalContainer}
disabled={disabled}
onClick={this.removeItem.bind(this, rowIndex + offset)}
testIdBuilder={testIdBuilder?.getChild(
`delRow-${rowIndex + offset}`
)}
>
{props.deleteBtnIcon ? (
<Icon
@ -1606,7 +1624,8 @@ export default class FormTable extends React.Component<TableProps, TableState> {
showFooterAddBtn,
footerAddBtn,
toolbarClassName,
onEvent
onEvent,
testIdBuilder
} = this.props;
const maxLength = this.resolveVariableProps(this.props, 'maxLength');
@ -1666,7 +1685,8 @@ export default class FormTable extends React.Component<TableProps, TableState> {
offset,
rowClassName,
rowClassNameExpr,
onPristineChange: this.handlePristineChange
onPristineChange: this.handlePristineChange,
testIdBuilder: testIdBuilder?.getChild('table')
}
)}
{(!isStatic &&
@ -1689,7 +1709,8 @@ export default class FormTable extends React.Component<TableProps, TableState> {
},
{
disabled: this.computedAddBtnDisabled(),
onClick: () => this.addItem(this.state.items.length)
onClick: () => this.addItem(this.state.items.length),
testIdBuilder: testIdBuilder?.getChild('add')
}
)
: null}
@ -1705,7 +1726,8 @@ export default class FormTable extends React.Component<TableProps, TableState> {
perPage,
total: this.state.items.length,
onPageChange: this.handlePageChange,
className: 'InputTable-pager'
className: 'InputTable-pager',
testIdBuilder: testIdBuilder?.getChild('page')
}
)
: null}

View File

@ -577,7 +577,8 @@ export default class TagControl extends React.PureComponent<
valueField,
env,
mobileUI,
labelField
labelField,
testIdBuilder
} = this.props;
const term = this.state.inputValue;
@ -635,6 +636,7 @@ export default class TagControl extends React.PureComponent<
popOverContainer={popOverContainer || env.getModalContainer}
allowInput={!mobileUI || (mobileUI && !options?.length)}
mobileUI={mobileUI}
testIdBuilder={testIdBuilder?.getChild('resule-box')}
>
{loading ? (
<Spinner loadingConfig={loadingConfig} size="sm" />
@ -716,6 +718,7 @@ export default class TagControl extends React.PureComponent<
options={finnalOptions}
itemRender={this.renderItem}
highlightIndex={highlightedIndex}
testIdBuilder={testIdBuilder?.getChild('options')}
getItemProps={({
item,
index

View File

@ -19,9 +19,7 @@ import {
createObject,
setVariable,
ucFirst,
isEffectiveApi,
getTestId,
buildTestId
isEffectiveApi
} from 'amis-core';
import {Icon, SpinnerExtraProps, Input, Spinner, OverflowTpl} from 'amis-ui';
import {ActionSchema} from '../Action';
@ -119,8 +117,6 @@ export interface TextControlSchema extends FormOptionsSchema {
/** 在内容为空的时候清除值 */
clearValueOnEmpty?: boolean;
testid?: string;
}
export type InputTextRendererEvent =
@ -730,8 +726,8 @@ export default class TextControl extends React.PureComponent<
themeCss,
css,
id,
testid,
nativeAutoComplete
nativeAutoComplete,
testIdBuilder
} = this.props;
let type = this.props.type?.replace(/^(?:native|input)\-/, '');
@ -810,7 +806,7 @@ export default class TextControl extends React.PureComponent<
}
)}
onClick={this.handleClick}
{...buildTestId(testid, data)}
{...testIdBuilder?.getTestId()}
>
<>
{filteredPlaceholder &&
@ -988,8 +984,8 @@ export default class TextControl extends React.PureComponent<
themeCss,
css,
id,
testid,
nativeAutoComplete
nativeAutoComplete,
testIdBuilder
} = this.props;
const type = this.props.type?.replace(/^(?:native|input)\-/, '');
@ -1017,7 +1013,7 @@ export default class TextControl extends React.PureComponent<
inputControlClassName,
inputOnly ? className : ''
)}
{...buildTestId(testid, data)}
{...testIdBuilder?.getTestId()}
>
{prefix ? (
<span className={cx('TextControl-inputPrefix')}>
@ -1045,7 +1041,7 @@ export default class TextControl extends React.PureComponent<
className={cx(nativeInputClassName, {
'TextControl-input-password': type === 'password' && revealPassword
})}
{...buildTestId(testid && `${testid}-input`)}
{...testIdBuilder?.getChild('input').getTestId()}
/>
{clearable && !disabled && !readOnly && value ? (
<a onClick={this.clearValue} className={cx('TextControl-clear')}>

View File

@ -473,7 +473,8 @@ export default class TreeControl extends React.Component<TreeProps, TreeState> {
searchable,
searchConfig = {},
heightAuto,
mobileUI
mobileUI,
testIdBuilder
} = this.props;
let {highlightTxt} = this.props;
const {filteredOptions, keyword} = this.state;
@ -537,6 +538,7 @@ export default class TreeControl extends React.Component<TreeProps, TreeState> {
itemRender={menuTpl ? this.renderOptionItem : undefined}
enableDefaultIcon={enableDefaultIcon}
mobileUI={mobileUI}
testIdBuilder={testIdBuilder?.getChild('tree')}
/>
);
@ -546,6 +548,7 @@ export default class TreeControl extends React.Component<TreeProps, TreeState> {
'is-sticky': searchable && searchConfig?.sticky,
'h-auto': heightAuto
})}
{...testIdBuilder?.getChild('control').getTestId()}
>
<Spinner
size="sm"
@ -566,6 +569,7 @@ export default class TreeControl extends React.Component<TreeProps, TreeState> {
{...omit(searchConfig, 'className', 'sticky')}
onSearch={this.handleSearch}
mobileUI={mobileUI}
testIdBuilder={testIdBuilder?.getChild('search')}
/>
{TreeCmpt}
</>

View File

@ -180,7 +180,8 @@ export default class ListControl extends React.Component<ListProps, any> {
data,
labelField,
listClassName,
translate: __
translate: __,
testIdBuilder
} = this.props;
let body: JSX.Element | null = null;
@ -202,6 +203,9 @@ export default class ListControl extends React.Component<ListProps, any> {
? this.handleDBClick.bind(this, option)
: undefined
}
{...testIdBuilder
?.getChild(`options-${option.value || key}`)
.getTestId()}
>
{itemSchema
? render(

View File

@ -17,6 +17,8 @@ import {ApiObject, ActionObject, isMobile} from 'amis-core';
import {FormBaseControlSchema, SchemaApi} from '../../Schema';
import {supportStatic} from './StaticHoc';
import type {TestIdBuilder} from 'amis-core';
/**
* Matrix
* https://aisuda.bce.baidu.com/amis/zh-CN/components/form/matrix
@ -88,6 +90,7 @@ export interface MatrixProps extends FormControlProps, SpinnerExtraProps {
*
*/
xCheckAll?: boolean;
testIdBuilder?: TestIdBuilder;
}
export interface MatrixState {
@ -405,7 +408,8 @@ export default class MatrixCheckbox extends React.Component<
multiple,
textAlign,
xCheckAll,
yCheckAll
yCheckAll,
testIdBuilder
} = this.props;
const value = this.props.value || buildDefaultValue(columns, rows);
@ -453,6 +457,7 @@ export default class MatrixCheckbox extends React.Component<
onChange={(checked: boolean) =>
this.toggleRowCheckAll(checked, value, y)
}
testIdBuilder={testIdBuilder?.getChild(y)}
/>
) : null}
{row.label}
@ -478,6 +483,7 @@ export default class MatrixCheckbox extends React.Component<
onChange={(checked: boolean) =>
this.toggleItem(checked, x, y)
}
testIdBuilder={testIdBuilder?.getChild(`${x}-${y}`)}
/>
</td>
))}

View File

@ -680,7 +680,8 @@ export default class PickerControl extends React.PureComponent<
valueField,
embed,
source,
strictMode
strictMode,
testIdBuilder
} = this.props;
const {maxTagCount, overflowTagPopoverInCRUD, displayPosition} =
this.getOverflowConfig();
@ -693,6 +694,7 @@ export default class PickerControl extends React.PureComponent<
multiple,
strictMode,
onSelect: embed ? this.handleSelect : undefined,
testIdBuilder: testIdBuilder?.getChild('body-schema'),
ref: this.crudRef,
popOverContainer,
...(embed ||
@ -727,7 +729,8 @@ export default class PickerControl extends React.PureComponent<
themeCss,
css,
id,
classPrefix: ns
classPrefix: ns,
testIdBuilder
} = this.props;
return (
<div className={cx(`PickerControl`, {'is-mobile': mobileUI}, className)}>
@ -768,7 +771,10 @@ export default class PickerControl extends React.PureComponent<
</div>
) : null}
<div className={cx('Picker-valueWrap')}>
<div
className={cx('Picker-valueWrap')}
{...testIdBuilder?.getTestId()}
>
{this.renderValues()}
<input
@ -788,7 +794,11 @@ export default class PickerControl extends React.PureComponent<
</a>
) : null}
<span onClick={this.open} className={cx('Picker-btn')}>
<span
onClick={this.open}
className={cx('Picker-btn')}
{...testIdBuilder?.getChild('picker-open-btn').getTestId()}
>
<Icon
icon="window-restore"
className={cx(
@ -817,7 +827,8 @@ export default class PickerControl extends React.PureComponent<
className: modalClassName,
body: {
children: this.renderBody
}
},
testIdBuilder: testIdBuilder?.getChild('modal')
},
{
key: 'modal',

View File

@ -6,7 +6,8 @@ import {
OptionsControlProps,
Option,
FormOptionsControl,
resolveEventData
resolveEventData,
TestIdBuilder
} from 'amis-core';
import {autobind, isEmpty, createObject} from 'amis-core';
import {ActionObject} from 'amis-core';
@ -37,6 +38,7 @@ export interface RadiosProps extends OptionsControlProps {
labelClassName?: string;
/** 选项CSS类名 */
optionClassName?: string;
testIdBuilder?: TestIdBuilder;
}
export default class RadiosControl extends React.Component<RadiosProps, any> {
@ -126,7 +128,8 @@ export default class RadiosControl extends React.Component<RadiosProps, any> {
data,
translate: __,
optionType,
level
level,
testIdBuilder
} = this.props;
return (
@ -151,6 +154,7 @@ export default class RadiosControl extends React.Component<RadiosProps, any> {
itemClassName={itemClassName}
optionType={optionType}
level={level}
testIdBuilder={testIdBuilder}
/>
);
}

View File

@ -14,7 +14,8 @@ import {
isEffectiveApi,
isApiOutdated,
createObject,
autobind
autobind,
TestIdBuilder
} from 'amis-core';
import {TransferDropDown, Spinner, Select, SpinnerExtraProps} from 'amis-ui';
import {FormOptionsSchema, SchemaApi} from '../../Schema';
@ -154,9 +155,9 @@ export interface SelectControlSchema
*
*/
filterOption?: 'string';
testid?: string;
};
testIdBuilder?: TestIdBuilder;
}
export interface SelectProps extends OptionsControlProps, SpinnerExtraProps {
@ -411,12 +412,15 @@ export default class SelectControl extends React.Component<SelectProps, any> {
@autobind
renderMenu(option: Option, state: any) {
const {menuTpl, render, data, optionClassName} = this.props;
const {menuTpl, render, data, optionClassName, testIdBuilder} = this.props;
return render(`menu/${state.index}`, menuTpl, {
showNativeTitle: true,
className: cx('Select-option-content', optionClassName),
data: createObject(createObject(data, state), option)
data: createObject(createObject(data, state), option),
testIdBuilder: testIdBuilder?.getChild(
'option-' + option.value || state.index
)
});
}

View File

@ -54,8 +54,6 @@ export interface SwitchControlSchema extends FormBaseControlSchema {
/** 是否处于加载状态 */
loading?: boolean;
testid?: string;
}
export interface SwitchProps extends FormControlProps, SpinnerExtraProps {
@ -147,7 +145,7 @@ export default class SwitchControl extends React.Component<SwitchProps, any> {
disabled,
loading,
loadingConfig,
testid
testIdBuilder
} = this.props;
const {on, off} = this.getResult();
@ -167,7 +165,7 @@ export default class SwitchControl extends React.Component<SwitchProps, any> {
size={size as any}
loading={loading}
loadingConfig={loadingConfig}
testid={testid}
testIdBuilder={testIdBuilder}
/>
)}
</div>

View File

@ -308,7 +308,8 @@ export class TabsTransferRenderer extends BaseTabsTransferRenderer<TabsTransferP
menuTpl,
data,
mobileUI,
initiallyOpen = true
initiallyOpen = true,
testIdBuilder
} = this.props;
return (
@ -343,6 +344,7 @@ export class TabsTransferRenderer extends BaseTabsTransferRenderer<TabsTransferP
ctx={data}
mobileUI={mobileUI}
initiallyOpen={initiallyOpen}
testIdBuilder={testIdBuilder}
/>
<Spinner

View File

@ -59,8 +59,6 @@ export interface TextareaControlSchema extends FormBaseControlSchema {
*
*/
resetValue?: string;
testid?: string;
}
export type TextAreaRendererEvent = 'blur' | 'focus' | 'change';

View File

@ -637,7 +637,8 @@ export class BaseTransferRenderer<
popOverContainer,
data,
autoCheckChildren = true,
initiallyOpen = true
initiallyOpen = true,
testIdBuilder
} = this.props;
// 目前 LeftOptions 没有接口可以动态加载
@ -729,6 +730,7 @@ export class BaseTransferRenderer<
onPageChange={this.handlePageChange}
initiallyOpen={initiallyOpen}
autoCheckChildren={autoCheckChildren}
testIdBuilder={testIdBuilder}
/>
<Spinner
overlay

View File

@ -33,6 +33,7 @@ import {FormOptionsSchema, SchemaApi} from '../../Schema';
import {supportStatic} from './StaticHoc';
import {TooltipWrapperSchema} from '../TooltipWrapper';
import type {ItemRenderStates} from 'amis-ui/lib/components/Selection';
import type {TestIdBuilder} from 'amis-core';
/**
* Tree
@ -130,6 +131,7 @@ export interface TreeSelectControlSchema extends FormOptionsSchema {
* Icontrue
*/
enableDefaultIcon?: boolean;
testIdBuilder?: TestIdBuilder;
}
export interface TreeSelectProps
@ -677,7 +679,8 @@ export default class TreeSelectControl extends React.Component<
itemHeight,
menuTpl,
enableDefaultIcon,
mobileUI
mobileUI,
testIdBuilder
} = this.props;
let filtedOptions =
@ -741,6 +744,7 @@ export default class TreeSelectControl extends React.Component<
itemRender={menuTpl ? this.renderOptionItem : undefined}
enableDefaultIcon={enableDefaultIcon}
mobileUI={mobileUI}
testIdBuilder={testIdBuilder}
/>
);
}
@ -768,7 +772,8 @@ export default class TreeSelectControl extends React.Component<
overflowTagPopover,
translate: __,
env,
loadingConfig
loadingConfig,
testIdBuilder
} = this.props;
const {isOpened} = this.state;
const resultValue = multiple
@ -778,7 +783,11 @@ export default class TreeSelectControl extends React.Component<
: '';
return (
<div ref={this.container} className={cx(`TreeSelectControl`, className)}>
<div
ref={this.container}
className={cx(`TreeSelectControl`, className)}
{...testIdBuilder?.getTestId()}
>
<ResultBox
popOverContainer={popOverContainer || env.getModalContainer}
maxTagCount={maxTagCount}
@ -815,6 +824,7 @@ export default class TreeSelectControl extends React.Component<
hasDropDownArrow
readOnly={mobileUI}
mobileUI={mobileUI}
testIdBuilder={testIdBuilder?.getChild('result-box')}
>
{loading ? (
<Spinner loadingConfig={loadingConfig} size="sm" />

View File

@ -5,8 +5,7 @@ import {
RendererProps,
buildStyle,
CustomStyle,
setThemeClassName,
buildTestId
setThemeClassName
} from 'amis-core';
import pick from 'lodash/pick';
import {BaseSchema, SchemaClassName, SchemaCollection} from '../Schema';
@ -241,7 +240,6 @@ export default class Grid<T> extends React.Component<GridProps & T, object> {
})
)}
style={styleVar}
{...buildTestId(testid)}
>
{this.renderColumns(this.props.columns)}
<Spinner loadingConfig={loadingConfig} overlay show={loading} />

View File

@ -1,5 +1,5 @@
import React from 'react';
import {buildTestId, Renderer, RendererProps} from 'amis-core';
import {Renderer, RendererProps} from 'amis-core';
import {Api, SchemaNode, Schema, ActionObject} from 'amis-core';
import {isVisible} from 'amis-core';
import {BaseSchema, SchemaObject} from '../Schema';
@ -215,11 +215,7 @@ export default class Grid2D extends React.Component<Grid2DProps, object> {
gridTemplateRows: templateRows.join(' ')
};
return (
<div style={curStyle} {...buildTestId(testid)}>
{this.renderGrids()}
</div>
);
return <div style={curStyle}>{this.renderGrids()}</div>;
}
}

View File

@ -959,7 +959,8 @@ export default class List extends React.Component<ListProps, object> {
checkOnItemClick,
itemAction,
classnames: cx,
translate: __
translate: __,
testIdBuilder
} = this.props;
const hasClickActions =
onEvent &&
@ -979,6 +980,7 @@ export default class List extends React.Component<ListProps, object> {
'is-modified': item.modified,
'is-moved': item.moved
}),
testIdBuilder: testIdBuilder?.getChild(index),
selectable: store.selectable,
checkable: item.checkable,
multiple,
@ -1180,7 +1182,8 @@ export class ListItem extends React.Component<ListItemProps> {
hideCheckToggler,
checkOnItemClick,
classnames: cx,
classPrefix: ns
classPrefix: ns,
testIdBuilder
} = this.props;
if (dragging) {
@ -1199,6 +1202,7 @@ export class ListItem extends React.Component<ListItemProps> {
checked={selected}
onChange={this.handleCheck}
inline
testIdBuilder={testIdBuilder?.getChild('checkbox')}
/>
</div>
);

View File

@ -175,8 +175,6 @@ export interface NavSchema extends BaseSchema {
*/
source?: SchemaApi;
testid?: string;
/**
* api source
*/
@ -854,8 +852,8 @@ export class Navigation extends React.Component<
render,
popOverContainer,
env,
testid,
searchable
searchable,
testIdBuilder
} = this.props;
const {dropIndicator, filteredLinks} = this.state;
@ -919,7 +917,7 @@ export class Navigation extends React.Component<
isOpen={(item: NavigationItem) => !!item.open}
stacked={!!stacked}
mode={mode}
testid={testid}
testIdBuilder={testIdBuilder}
themeColor={themeColor}
onSelect={this.handleClick}
onToggle={this.toggleLink}

View File

@ -15,7 +15,7 @@ import {
import {BaseSchema, SchemaClassName} from '../Schema';
import {SearchBox} from 'amis-ui';
import type {ListenerAction} from 'amis-core';
import {ListenerAction, TestIdBuilder} from 'amis-core';
import type {SpinnerExtraProps} from 'amis-ui';
/**
@ -82,6 +82,7 @@ interface SearchBoxProps
name: string;
onQuery?: (query: {[propName: string]: string}) => any;
loading?: boolean;
testIdBuilder?: TestIdBuilder;
}
export interface SearchBoxState {
@ -212,7 +213,8 @@ export class SearchBoxRenderer extends React.Component<
mobileUI,
loading,
loadingConfig,
onEvent
onEvent,
testIdBuilder
} = this.props;
const value = this.state.value;
/** 有可能通过Search事件处理 */
@ -241,6 +243,7 @@ export class SearchBoxRenderer extends React.Component<
onFocus={() => this.dispatchEvent('focus')}
onBlur={() => this.dispatchEvent('blur')}
mobileUI={mobileUI}
testIdBuilder={testIdBuilder}
/>
);
}

View File

@ -21,7 +21,8 @@ import {
isVisible,
qsstringify,
createObject,
extendObject
extendObject,
TestIdBuilder
} from 'amis-core';
import {
BaseSchema,
@ -151,6 +152,7 @@ export interface ServiceProps
Omit<ServiceSchema, 'type' | 'className'> {
store: IServiceStore;
messages: SchemaMessage;
testIdBuilder?: TestIdBuilder;
}
export default class Service extends React.Component<ServiceProps> {
timer: ReturnType<typeof setTimeout>;
@ -800,11 +802,16 @@ export default class Service extends React.Component<ServiceProps> {
classPrefix: ns,
classnames: cx,
loadingConfig,
showErrorMsg
showErrorMsg,
testIdBuilder
} = this.props;
return (
<div className={cx(`${ns}Service`, className)} style={style}>
<div
className={cx(`${ns}Service`, className)}
style={style}
{...testIdBuilder?.getTestId()}
>
{!env.forceSilenceInsideError &&
store.error &&
showErrorMsg !== false ? (

View File

@ -39,7 +39,8 @@ export function AutoFilterForm({
onSearchableFromReset,
onSearchableFromSubmit,
onSearchableFromInit,
popOverContainer
popOverContainer,
testIdBuilder
}: AutoFilterFormProps) {
const schema = React.useMemo(() => {
const {columnsNum, showBtnToolbar} =
@ -58,11 +59,13 @@ export function AutoFilterForm({
? {
type: 'input-text',
name: column.name,
label: column.label
label: column.label,
testIdBuilder: testIdBuilder.getChild(column.name)
}
: {
type: 'input-text',
name: column.name,
testIdBuilder: testIdBuilder.getChild(column.name),
...column.searchable
}),
name: column.searchable?.name ?? column.name,
@ -98,6 +101,8 @@ export function AutoFilterForm({
tpl: ''
});
}
const moreTestIdBuilder = testIdBuilder?.getChild('more');
lastGroup.body.push({
type: 'container',
className: 'AutoFilterToolbar',
@ -112,6 +117,7 @@ export function AutoFilterForm({
size: 'sm',
align: 'right',
visible: showBtnToolbar,
testIdBuilder: moreTestIdBuilder,
buttons: searchableColumns.map(column => {
return {
children: ({render}: any) =>
@ -124,6 +130,7 @@ export function AutoFilterForm({
inputClassName: cx('Table-searchableForm-checkbox-inner'),
name: `__whatever_name`,
option: column.searchable?.label ?? column.label,
testIdBuilder: moreTestIdBuilder?.getChild(column.name),
badge: {
offset: [-10, 5],
visibleOn: `${

View File

@ -7,7 +7,8 @@ import {
ThemeProps,
resolveVariable,
buildTrackExpression,
evalTrackExpression
evalTrackExpression,
TestIdBuilder
} from 'amis-core';
import {BadgeObject, Checkbox, Icon, Spinner} from 'amis-ui';
import React from 'react';
@ -33,6 +34,7 @@ export interface CellProps extends ThemeProps {
quickEditFormRef: any;
onImageEnlarge?: any;
translate: (key: string, ...args: Array<any>) => string;
testIdBuilder?: TestIdBuilder;
}
export default function Cell({
@ -53,7 +55,8 @@ export default function Cell({
popOverContainer,
quickEditFormRef,
onImageEnlarge,
translate: __
translate: __,
testIdBuilder
}: CellProps) {
if (column.name && item.rowSpans[column.name] === 0) {
return null;
@ -77,6 +80,7 @@ export default function Cell({
<td
style={style}
className={cx(column.pristine.className, stickyClassName)}
{...testIdBuilder?.getTestId()}
>
<Checkbox
classPrefix={ns}
@ -85,6 +89,7 @@ export default function Cell({
checked={item.checked || item.partial}
disabled={item.checkdisable || !item.checkable}
onChange={onCheckboxChange}
testIdBuilder={testIdBuilder?.getChild('chekbx')}
/>
</td>
);
@ -95,6 +100,7 @@ export default function Cell({
className={cx(column.pristine.className, stickyClassName, {
'is-dragDisabled': !item.draggable
})}
{...testIdBuilder?.getChild('drag').getTestId()}
>
{item.draggable ? <Icon icon="drag" className="icon" /> : null}
</td>
@ -111,6 +117,9 @@ export default function Cell({
// data-tooltip="展开/收起"
// data-position="top"
onClick={item.toggleExpanded}
{...testIdBuilder
?.getChild(item.expanded ? 'fold' : 'expand')
.getTestId()}
>
<Icon icon="right-arrow-bold" className="icon" />
</a>
@ -142,6 +151,7 @@ export default function Cell({
key="retryBtn"
onClick={item.resetDefered}
data-tooltip={__('Options.retry', {reason: item.error})}
{...testIdBuilder?.getChild('retry').getTestId()}
>
<Icon icon="retry" className="icon" />
</a>
@ -152,6 +162,9 @@ export default function Cell({
// data-tooltip="展开/收起"
// data-position="top"
onClick={item.toggleExpanded}
{...testIdBuilder
?.getChild(item.expanded ? 'fold' : 'expand')
.getTestId()}
>
<Icon icon="right-arrow-bold" className="icon" />
</a>
@ -174,6 +187,7 @@ export default function Cell({
draggable
onDragStart={onDragStart}
className={cx('Table-dragBtn')}
{...testIdBuilder?.getChild('drag').getTestId()}
>
<Icon icon="drag" className="icon" />
</a>
@ -240,7 +254,8 @@ export default function Cell({
column.pristine.className,
stickyClassName,
addtionalClassName
)
),
testIdBuilder: testIdBuilder?.getChild(column.name || column.value)
};
delete subProps.label;

View File

@ -1,5 +1,5 @@
import React from 'react';
import {ClassNamesFn, RendererEvent} from 'amis-core';
import {ClassNamesFn, RendererEvent, autobind} from 'amis-core';
import {SchemaNode, ActionObject} from 'amis-core';
import TableRow from './TableRow';
@ -9,7 +9,7 @@ import {trace, reaction} from 'mobx';
import {createObject, flattenTree} from 'amis-core';
import {LocaleProps} from 'amis-core';
import {ActionSchema} from '../Action';
import type {IColumn, IRow, ITableStore} from 'amis-core';
import type {IColumn, IRow, ITableStore, TestIdBuilder} from 'amis-core';
export interface TableBodyProps extends LocaleProps {
store: ITableStore;
@ -60,6 +60,7 @@ export interface TableBodyProps extends LocaleProps {
prefixRow?: Array<any>;
affixRow?: Array<any>;
itemAction?: ActionSchema;
testIdBuilder?: TestIdBuilder;
}
@observer
@ -68,10 +69,16 @@ export class TableBody extends React.Component<TableBodyProps> {
this.props.store.initTableWidth();
}
@autobind
testIdBuilder(rowPath: string) {
return this.props.testIdBuilder?.getChild(`row-${rowPath}`);
}
renderRows(
rows: Array<any>,
columns = this.props.columns,
rowProps: any = {}
rowProps: any = {},
indexPath?: string
): any {
const {
rowClassName,
@ -97,15 +104,19 @@ export class TableBody extends React.Component<TableBodyProps> {
return rows.map((item: IRow, rowIndex: number) => {
const itemProps = buildItemProps ? buildItemProps(item, rowIndex) : null;
const rowPath = `${indexPath ? indexPath + '/' : ''}${rowIndex}`;
const doms = [
<TableRow
{...itemProps}
testIdBuilder={this.testIdBuilder}
store={store}
itemAction={itemAction}
classnames={cx}
checkOnItemClick={checkOnItemClick}
key={item.id}
itemIndex={rowIndex}
rowPath={rowPath}
item={item}
itemClassName={cx(
rowClassNameExpr
@ -144,6 +155,7 @@ export class TableBody extends React.Component<TableBodyProps> {
checkOnItemClick={checkOnItemClick}
key={`foot-${item.id}`}
itemIndex={rowIndex}
rowPath={rowPath}
item={item}
itemClassName={cx(
rowClassNameExpr
@ -164,16 +176,22 @@ export class TableBody extends React.Component<TableBodyProps> {
onQuickChange={onQuickChange}
ignoreFootableContent={ignoreFootableContent}
{...rowProps}
testIdBuilder={this.testIdBuilder}
/>
);
}
} else if (item.children.length && item.expanded) {
// 嵌套表格
doms.push(
...this.renderRows(item.children, columns, {
...rowProps,
parent: item
})
...this.renderRows(
item.children,
columns,
{
...rowProps,
parent: item
},
rowPath
)
);
}
return doms;

View File

@ -10,10 +10,13 @@ import {Badge} from 'amis-ui';
import {ColorScale} from 'amis-core';
import {isPureVariable, resolveVariableAndFilter} from 'amis-core';
import type {TestIdBuilder} from 'amis-core';
export interface TableCellProps extends RendererProps {
wrapperComponent?: React.ElementType;
column: any;
contentsOnly?: boolean;
testIdBuilder?: TestIdBuilder;
}
export class TableCell extends React.Component<TableCellProps> {
@ -64,6 +67,7 @@ export class TableCell extends React.Component<TableCellProps> {
row,
showBadge,
itemBadge,
testIdBuilder,
...rest
} = this.props;
@ -158,6 +162,7 @@ export class TableCell extends React.Component<TableCellProps> {
className={cx(className)}
tabIndex={tabIndex}
onKeyUp={onKeyUp}
{...testIdBuilder?.getChild('cell').getTestId()}
>
{showBadge ? (
<Badge

View File

@ -15,13 +15,14 @@ import ItemActionsWrapper from './ItemActionsWrapper';
import {SchemaTpl} from '../../Schema';
import {Icon} from 'amis-ui';
import type {IColumn, IRow} from 'amis-core';
import type {IColumn, IRow, TestIdBuilder} from 'amis-core';
import ColGroup from './ColGroup';
export interface TableContentProps extends LocaleProps {
className?: string;
tableClassName?: string;
classnames: ClassNamesFn;
testIdBuilder: TestIdBuilder;
columns: Array<IColumn>;
columnsGroup: Array<{
label: string;
@ -173,7 +174,8 @@ export class TableContent extends React.PureComponent<TableContentProps> {
store,
dispatchEvent,
onEvent,
loading
loading,
testIdBuilder
} = this.props;
const tableClassName = cx('Table-table', this.props.tableClassName);
@ -299,6 +301,7 @@ export class TableContent extends React.PureComponent<TableContentProps> {
prefixRow={prefixRow}
affixRow={affixRow}
data={data}
testIdBuilder={testIdBuilder}
rowsProps={{
dispatchEvent,
onEvent

View File

@ -5,6 +5,7 @@ import {
ITableStore,
RendererEvent,
RendererProps,
TestIdBuilder,
autobind,
setVariable,
traceProps
@ -44,6 +45,8 @@ interface TableRowProps extends Pick<RendererProps, 'render'> {
regionPrefix?: string;
checkOnItemClick?: boolean;
ignoreFootableContent?: boolean;
testIdBuilder?: (key: string) => TestIdBuilder;
rowPath: string; // 整体行的路径,树形时需要父行序号/当前展开层级下的行序号
[propName: string]: any;
}
@ -198,7 +201,8 @@ export class TableRow extends React.PureComponent<
checkdisable,
trRef,
isNested,
testIdBuilder,
rowPath,
...rest
} = this.props;
@ -261,6 +265,7 @@ export class TableRow extends React.PureComponent<
width: null,
rowIndex: itemIndex,
colIndex: column.index,
rowPath,
key: column.index,
onAction: this.handleAction,
onQuickChange: this.handleQuickChange,
@ -314,6 +319,7 @@ export class TableRow extends React.PureComponent<
},
`Table-tr--${depth}th`
)}
{...testIdBuilder?.(rowPath).getTestId()}
>
{columns.map(column =>
appeard ? (
@ -321,6 +327,7 @@ export class TableRow extends React.PureComponent<
...rest,
rowIndex: itemIndex,
colIndex: column.index,
rowPath,
key: column.id,
onAction: this.handleAction,
onQuickChange: this.handleQuickChange,

View File

@ -41,8 +41,7 @@ import {
resizeSensor,
offset,
getStyleNumber,
getPropValue,
buildTestId
getPropValue
} from 'amis-core';
import {
Button,
@ -1677,7 +1676,8 @@ export default class Table extends React.Component<TableProps, object> {
translate: __,
query,
data,
autoGenerateFilter
autoGenerateFilter,
testIdBuilder
} = this.props;
const searchableColumns = store.searchableColumns;
@ -1697,6 +1697,7 @@ export default class Table extends React.Component<TableProps, object> {
onSearchableFromSubmit={onSearchableFromSubmit}
onSearchableFromInit={onSearchableFromInit}
popOverContainer={this.getPopOverContainer}
testIdBuilder={testIdBuilder.getChild('filter')}
/>
);
}
@ -2096,7 +2097,8 @@ export default class Table extends React.Component<TableProps, object> {
classnames: cx,
canAccessSuperData,
itemBadge,
translate
translate,
testIdBuilder
} = this.props;
return (
@ -2120,6 +2122,9 @@ export default class Table extends React.Component<TableProps, object> {
quickEditFormRef={this.subFormRef}
onImageEnlarge={this.handleImageEnlarge}
translate={translate}
testIdBuilder={testIdBuilder.getChild(
`cell-${props.rowPath}-${column.index}`
)}
/>
);
}
@ -2685,7 +2690,9 @@ export default class Table extends React.Component<TableProps, object> {
itemActions,
dispatchEvent,
onEvent,
loadingConfig
loadingConfig,
testIdBuilder,
data
} = this.props;
// 理论上来说 store.rows 应该也行啊
@ -2701,6 +2708,7 @@ export default class Table extends React.Component<TableProps, object> {
itemActions
})}
<TableContent
testIdBuilder={testIdBuilder}
tableClassName={cx(
{
'Table-table--checkOnItemClick': checkOnItemClick,
@ -2805,7 +2813,7 @@ export default class Table extends React.Component<TableProps, object> {
autoFillHeight,
autoGenerateFilter,
mobileUI,
testid
testIdBuilder
} = this.props;
this.renderedToolbars = []; // 用来记录哪些 toolbar 已经渲染了,已经渲染了就不重复渲染了。
@ -2824,7 +2832,7 @@ export default class Table extends React.Component<TableProps, object> {
'Table--autoFillHeight': autoFillHeight
})}
style={store.buildStyles(style)}
{...buildTestId(testid)}
{...testIdBuilder.getTestId()}
>
{autoGenerateFilter ? this.renderAutoFilterForm() : null}
{this.renderAffixHeader(tableClassName)}

View File

@ -8,8 +8,7 @@ import {
RendererProps,
resolveMappingObject,
CustomStyle,
setThemeClassName,
buildTestId
setThemeClassName
} from 'amis-core';
import {BaseSchema, SchemaObject} from '../Schema';
@ -300,7 +299,6 @@ export default class TableView extends React.Component<TableViewProps, object> {
})
)}
style={{width: width, borderCollapse: 'collapse'}}
{...buildTestId(testid)}
>
{this.renderCaption()}
{this.renderCols()}

View File

@ -769,7 +769,8 @@ export default class Tabs extends React.Component<TabsProps, TabsState> {
collapseBtnLabel,
disabled,
mobileUI,
swipeable
swipeable,
testIdBuilder
} = this.props;
const mode = tabsMode || dMode;
@ -815,6 +816,9 @@ export default class Tabs extends React.Component<TabsProps, TabsState> {
: unmountOnExit
}
onSelect={this.handleSelect}
testIdBuilder={testIdBuilder?.getChild(
`tab-${typeof tab.title === 'string' ? tab.title : index}`
)}
>
{render(
`item/${index}`,
@ -856,6 +860,9 @@ export default class Tabs extends React.Component<TabsProps, TabsState> {
: unmountOnExit
}
onSelect={this.handleSelect}
testIdBuilder={testIdBuilder?.getChild(
`tab-${typeof tab.title === 'string' ? tab.title : index}`
)}
>
{this.renderTab
? this.renderTab(tab, this.props, index)
@ -903,6 +910,7 @@ export default class Tabs extends React.Component<TabsProps, TabsState> {
collapseOnExceed={collapseOnExceed}
collapseBtnLabel={collapseBtnLabel}
mobileUI={mobileUI}
testIdBuilder={testIdBuilder}
>
{children}
</CTabs>

View File

@ -7,7 +7,7 @@ import {
CustomStyle,
setThemeClassName
} from 'amis-core';
import {filter, asyncFilter} from 'amis-core';
import {filter, asyncFilter, TestIdBuilder} from 'amis-core';
import isEmpty from 'lodash/isEmpty';
import {anyChanged, getPropValue} from 'amis-core';
import {escapeHtml} from 'amis-core';
@ -52,6 +52,8 @@ export interface TplSchema extends BaseSchema {
*
*/
badge?: BadgeObject;
testidBuilder?: TestIdBuilder;
}
export interface TplProps extends RendererProps, TplSchema {
@ -200,7 +202,8 @@ export class Tpl extends React.Component<TplProps, TplState> {
id,
wrapperCustomStyle,
env,
themeCss
themeCss,
testIdBuilder
} = this.props;
const Component = wrapperComponent || (inline ? 'span' : 'div');
const {content} = this.state;
@ -236,6 +239,7 @@ export class Tpl extends React.Component<TplProps, TplState> {
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
{...testIdBuilder?.getChild('tpl')?.getTestId()}
>
<span
className={cln ? cx(cln) : undefined}

View File

@ -1,5 +1,5 @@
import React from 'react';
import {buildTestId, Renderer, RendererProps} from 'amis-core';
import {Renderer, RendererProps} from 'amis-core';
import {BaseSchema, SchemaCollection} from '../Schema';
import {resolveVariable} from 'amis-core';
import {SchemaNode} from 'amis-core';
@ -82,7 +82,6 @@ export default class Wrapper extends React.Component<WrapperProps, object> {
className
)}
style={buildStyle(style, data)}
{...buildTestId(testid)}
>
{this.renderBody()}
</div>