diff --git a/packages/components/steps/Steps.tsx b/packages/components/steps/Steps.tsx index cd303fa981..68861bae90 100644 --- a/packages/components/steps/Steps.tsx +++ b/packages/components/steps/Steps.tsx @@ -54,13 +54,8 @@ const Steps = forwardRefWithStatics( return item.status; } // value 不存在时,使用 index 进行区分每一个步骤 - if (item.value === undefined) { - if (sequence === 'positive' && typeof current === 'number' && index < current) { - return 'finish'; - } - if (sequence === 'reverse' && typeof current === 'number' && index > current) { - return 'finish'; - } + if (item.value === undefined && typeof current === 'number' && index < current) { + return 'finish'; } // value 存在,找匹配位置 @@ -70,10 +65,7 @@ const Steps = forwardRefWithStatics( console.warn('TDesign Steps Warn: The current `value` is not exist.'); return 'default'; } - if (sequence === 'positive' && index < matchIndex) { - return 'finish'; - } - if (sequence === 'reverse' && index > matchIndex) { + if (index < matchIndex) { return 'finish'; } } @@ -83,27 +75,32 @@ const Steps = forwardRefWithStatics( } return 'default'; }, - [current, sequence, indexMap], + [current, indexMap], ); const stepItemList = useMemo(() => { if (options) { - const optionsDisplayList = sequence === 'reverse' ? options.reverse() : options; - return options.map((item, index) => { - const stepIndex = sequence === 'reverse' ? optionsDisplayList.length - index - 1 : index; - return ; + const optionsDisplayList = sequence === 'reverse' ? [...options].reverse() : options; + const optionsDisplayListLength = optionsDisplayList.length; + + return optionsDisplayList.map((item, index) => { + const stepIndex = sequence === 'reverse' ? optionsDisplayListLength - index - 1 : index; + return ( + + ); }); } const childrenList = React.Children.toArray(children); - const childrenDisplayList = sequence === 'reverse' ? childrenList.reverse() : childrenList; + const childrenDisplayList = sequence === 'reverse' ? [...childrenList].reverse() : childrenList; + const childrenDisplayListLength = childrenDisplayList.length; - return childrenList.map((child: React.ReactElement, index: number) => { - const stepIndex = sequence === 'reverse' ? childrenDisplayList.length - index - 1 : index; + return childrenDisplayList.map((child: React.ReactElement, index: number) => { + const stepIndex = sequence === 'reverse' ? childrenDisplayListLength - index - 1 : index; return React.cloneElement(child, { ...child.props, index: stepIndex, - status: handleStatus(child.props, index), + status: handleStatus(child.props, stepIndex), }); }); }, [options, children, sequence, handleStatus]); diff --git a/packages/components/steps/__tests__/steps.branches.test.tsx b/packages/components/steps/__tests__/steps.branches.test.tsx new file mode 100644 index 0000000000..75d02d7d98 --- /dev/null +++ b/packages/components/steps/__tests__/steps.branches.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { render, waitFor } from '@test/utils'; +import { vi } from 'vitest'; +import Steps from '../Steps'; +import StepItem from '../StepItem'; + +const TEST_ROOT_ID = 'step-test-branches'; + +describe('Steps branches', () => { + test('options reverse order and index mapping', async () => { + const opts = [ + { title: 'first', content: 'c1', value: 0 }, + { title: 'second', content: 'c2', value: 1 }, + { title: 'third', content: 'c3', value: 2 }, + ]; + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const titles = Array.from(root.querySelectorAll('.t-steps-item__title')).map((n) => n.textContent); + // when reverse, display order should be same length but titles still present + expect(titles.length).toBe(3); + expect(titles).toContain('first'); + expect(titles).toContain('second'); + expect(titles).toContain('third'); + }); + + test('children cloneElement branch and FINISH current', async () => { + const onChange = vi.fn?.() ?? (() => undefined); + const { getByTestId } = render( +
+ + + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + // all items should be finish + const finish = root.querySelectorAll('.t-steps-item--finish'); + expect(finish.length).toBe(2); + }); + + test('StepItem default icon number rendered when icon=true and theme=default', async () => { + const { getByTestId } = render( +
+ + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const number = root.querySelector('.t-steps-item__icon--number'); + expect(number).not.toBeNull(); + expect(number).toHaveTextContent('1'); + }); +}); diff --git a/packages/components/steps/__tests__/steps.extra.test.tsx b/packages/components/steps/__tests__/steps.extra.test.tsx new file mode 100644 index 0000000000..5ebbb9e36e --- /dev/null +++ b/packages/components/steps/__tests__/steps.extra.test.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { render, waitFor, fireEvent, vi } from '@test/utils'; +import Steps from '../Steps'; +import StepItem from '../StepItem'; +import StepsContext from '../StepsContext'; + +const TEST_ROOT_ID = 'step-test-root-extra'; + +const defaultOptions = [ + { title: '0', content: '这里', value: 0 }, + { title: '1', content: '这里', value: 1 }, + { title: '2', content: '这里', value: 2 }, +]; + +describe('Steps extra branches', () => { + test("current='FINISH' -> all items finish (options)", async () => { + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const finishes = root.querySelectorAll('.t-steps-item--finish'); + expect(finishes.length).toBe(defaultOptions.length); + }); + + test('item.status override (error) renders error class', async () => { + const opts = [ + { title: 'a', content: 'a', value: 0, status: 'error' as any }, + { title: 'b', content: 'b', value: 1 }, + ]; + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const errorItem = root.querySelector('.t-steps-item--error'); + expect(errorItem).not.toBeNull(); + }); + + test('current value not exist triggers console.warn and defaults to wait', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const opts = [ + { title: 'a', content: 'a', value: 'a' }, + { title: 'b', content: 'b', value: 'b' }, + ]; + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + expect(warnSpy).toHaveBeenCalledWith('TDesign Steps Warn: The current `value` is not exist.'); + const waitItems = root.querySelectorAll('.t-steps-item--wait'); + // both should be in default/wait state + expect(waitItems.length).toBe(opts.length); + warnSpy.mockRestore(); + }); + + test('status=process prevents click triggering onChange', async () => { + const onChange = vi.fn(); + const { getByTestId } = render( +
+ + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const inner = root.querySelector('.t-steps-item__inner'); + fireEvent.click(inner); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('icon=false results in no icon content', async () => { + const { getByTestId } = render( +
+ + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const icon = root.querySelector('.t-steps-item__icon'); + // icon container exists but should have no child nodes when icon=false + expect(icon).not.toBeNull(); + expect(icon.childNodes.length).toBe(0); + }); +}); diff --git a/packages/components/steps/__tests__/steps.test.tsx b/packages/components/steps/__tests__/steps.test.tsx index 11650e575a..67038eedef 100644 --- a/packages/components/steps/__tests__/steps.test.tsx +++ b/packages/components/steps/__tests__/steps.test.tsx @@ -1,86 +1,772 @@ -import React from 'react'; +import React, { useState } from 'react'; import { render, waitFor, fireEvent, vi } from '@test/utils'; +import { omit } from '@tdesign/common-js/utils/helper'; import Steps from '../Steps'; +import { TdStepsProps } from '../type'; const { StepItem } = Steps; -const stepOptions = [ - { - title: '1', - content: '这里是提示文字', - value: 1, - }, - { - title: '2', - content: '这里是提示文字', - value: 2, - }, - { - title: '3', - content: '这里是提示文字', - value: 3, - }, +type Layout = 'horizontal' | 'vertical'; +type Theme = 'default' | 'dot'; +type Sequence = 'positive' | 'reverse'; + +const defaultOptions = [ + { title: '0', content: '这里是提示文字', value: 0 }, + { title: '1', content: '这里是提示文字', value: 1 }, + { title: '2', content: '这里是提示文字', value: 2 }, ]; -describe('Steps 组件测试', () => { - test('mount, unmount 测试', () => { - const wrapper = render( - - - - - - , - ); - - expect(() => { - wrapper.unmount(); - }).not.toThrow(); +const TEST_ROOT_ID = 'step-test-root'; + +const StepRender = (props: TdStepsProps) => { + const [current, setCurrent] = useState(0); + return ( +
+ { + setCurrent(c); + props?.onChange && props.onChange(c, p); + }} + /> +
+ ); +}; + +const SlotRender = (props: TdStepsProps & { initial?: number }) => { + const [current, setCurrent] = useState(props.initial ?? 0); + return ( +
+ { + setCurrent(c); + props?.onChange && props.onChange(c, p); + }} + > + + + + +
+ ); +}; + +// --- Helpers to reduce repetition and assert node states --- +const nodeHasState = (el: Element | null, state: 'process' | 'finish' | 'wait') => { + if (!el) return false; + const cls = el.className || ''; + const re = new RegExp(`\\bt-steps-item(?:--|-)?${state}\\b`); + return re.test(cls); +}; + +const getStepItems = (root: Element) => Array.from(root.querySelectorAll('.t-steps-item')); + +const findProcessingIndex = (items: Element[]) => items.findIndex((it) => nodeHasState(it, 'process')); + +const verifyDistribution = (root: Element, sequence: Sequence = 'positive') => { + const items = getStepItems(root); + const active = findProcessingIndex(items); + expect(active).toBeGreaterThanOrEqual(0); + items.forEach((it, idx) => { + if (idx === active) { + expect(nodeHasState(it, 'process')).toBe(true); + } else if (sequence === 'positive') { + if (idx < active) expect(nodeHasState(it, 'finish')).toBe(true); + if (idx > active) expect(nodeHasState(it, 'wait')).toBe(true); + } else { + if (idx > active) expect(nodeHasState(it, 'finish')).toBe(true); + if (idx < active) expect(nodeHasState(it, 'wait')).toBe(true); + } }); +}; + +const clickEachAndVerify = async function clickEachAndVerify(root: Element, sequence: Sequence = 'positive') { + const icons = Array.from(root.querySelectorAll('.t-steps-item__icon')); + await icons.reduce( + (prev, icon) => + prev.then(async () => { + fireEvent.click(icon); + await waitFor(() => { + const items = getStepItems(root); + const hasProcess = items.some((it) => nodeHasState(it, 'process')); + if (!hasProcess) throw new Error('no process state yet'); + }); + verifyDistribution(root, sequence); + }), + Promise.resolve(), + ); +}; + +describe('Steps 组件测试', () => { + describe('基础功能', () => { + describe('渲染测试', () => { + test('mount/unmount - 组件正常挂载和卸载', () => { + const wrapper = render(); + expect(() => wrapper.unmount()).not.toThrow(); + }); + + test('options 模式 - 基本渲染', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items.length).toBe(3); + }); + + test('children 模式 - 基本渲染', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items.length).toBe(3); + }); + }); + + describe('layout 属性', () => { + test('layout=vertical - options模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + expect(root.querySelector('.t-steps')).toHaveClass('t-steps--vertical'); + }); + + test('layout=vertical - children模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + expect(root.querySelector('.t-steps')).toHaveClass('t-steps--vertical'); + }); + + test('layout=horizontal - options模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + expect(root.querySelector('.t-steps')).toHaveClass('t-steps--horizontal'); + }); + test('layout=horizontal - children模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + expect(root.querySelector('.t-steps')).toHaveClass('t-steps--horizontal'); + }); + }); + + describe('readonly 属性', () => { + test('readonly=true - options模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + expect(root.querySelectorAll('.t-steps-item--clickable').length).toBe(0); + }); - test('options 测试', async () => { - const testId = 'step options test'; - const handleChange = vi.fn(); + test('readonly=true - children模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + expect(root.querySelectorAll('.t-steps-item--clickable').length).toBe(0); + }); + }); - const { getByTestId } = render( -
- -
, - ); + describe('theme 属性', () => { + test('theme=default - options模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const numbers = root.querySelectorAll('.t-steps-item__icon--number'); + expect(numbers.length).toBe(3); + expect(numbers[0]).toHaveTextContent('1'); + }); - const stepsInstance = await waitFor(() => getByTestId(testId)); - const stepsItems = stepsInstance.querySelectorAll('.t-steps-item'); - expect(stepsItems.length).toBe(3); + test('theme=default - children模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const numbers = root.querySelectorAll('.t-steps-item__icon--number'); + expect(numbers.length).toBe(3); + expect(numbers[0]).toHaveTextContent('1'); + }); - fireEvent.click(stepsInstance.querySelector('.t-steps-item__inner')); - expect(handleChange).toBeCalledTimes(1); + test('theme=dot - options模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const numbers = root.querySelectorAll('.t-steps-item__icon--number'); + expect(numbers.length).toBe(0); + }); + + test('theme=dot - children模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const numbers = root.querySelectorAll('.t-steps-item__icon--number'); + expect(numbers.length).toBe(0); + }); + }); + + describe('separator 属性', () => { + test('separator=line - options模式', async () => { + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId('step-test-root-line')); + expect(root.querySelector('.t-steps')).toHaveClass('t-steps--line-separator'); + }); + + test('separator=line - children模式', async () => { + const { getByTestId } = render( +
+ + + + + +
, + ); + const root = await waitFor(() => getByTestId('step-test-root-line')); + expect(root.querySelector('.t-steps')).toHaveClass('t-steps--line-separator'); + }); + + test('separator=dashed - options模式', async () => { + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId('step-test-root-dashed')); + expect(root.querySelector('.t-steps')).toHaveClass('t-steps--dashed-separator'); + }); + + test('separator=dashed - children模式', async () => { + const { getByTestId } = render( +
+ + + + + +
, + ); + const root = await waitFor(() => getByTestId('step-test-root-dashed')); + expect(root.querySelector('.t-steps')).toHaveClass('t-steps--dashed-separator'); + }); + + test('separator=arrow - options模式', async () => { + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId('step-test-root-arrow')); + expect(root.querySelector('.t-steps')).toHaveClass('t-steps--arrow-separator'); + }); + + test('separator=arrow - children模式', async () => { + const { getByTestId } = render( +
+ + + + + +
, + ); + const root = await waitFor(() => getByTestId('step-test-root-arrow')); + expect(root.querySelector('.t-steps')).toHaveClass('t-steps--arrow-separator'); + }); + }); + + describe('数据源属性', () => { + test('options 数据驱动 - options模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const titles = root.querySelectorAll('.t-steps-item__title'); + expect(titles[0]).toHaveTextContent('0'); + expect(titles[1]).toHaveTextContent('1'); + expect(titles[2]).toHaveTextContent('2'); + }); + + test('children 自定义内容 - children模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const titles = root.querySelectorAll('.t-steps-item__title'); + expect(titles[0]).toHaveTextContent('登录'); + expect(titles[1]).toHaveTextContent('购物'); + expect(titles[2]).toHaveTextContent('支付'); + }); + + test('自定义 icon - children模式', async () => { + const { getByTestId } = render( +
+ + + 按钮} /> + + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const iconItems = root.querySelectorAll('.t-steps-item__icon'); + expect(iconItems.length).toBe(4); + expect((iconItems[1].children[0] as Element).tagName.toLowerCase()).toBe('button'); + }); + + test('自定义 icon - options模式', async () => { + const customIconOptions = [ + { title: '登录', content: '已完成状态', value: 0 }, + { title: '购物', content: '进行中状态', value: 1, icon: }, + { title: '支付', content: '未开始', value: 2 }, + { title: '完成', content: '未开始', value: 3 }, + ]; + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const iconItems = root.querySelectorAll('.t-steps-item__icon'); + expect(iconItems.length).toBe(4); + expect((iconItems[1].children[0] as Element).tagName.toLowerCase()).toBe('button'); + }); + }); + + describe('current 属性', () => { + test('current=FINISH - options模式', async () => { + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + items.forEach((item) => { + expect(item).toHaveClass('t-steps-item--finish'); + }); + }); + + test('current=FINISH - children模式', async () => { + const { getByTestId } = render( +
+ + + + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + items.forEach((item) => { + expect(item).toHaveClass('t-steps-item--finish'); + }); + }); + }); + + describe('自定义 status', () => { + test('自定义 status 优先级 - options模式', async () => { + const opts = [ + { title: '1', content: 'content1', value: 0, status: 'error' as const }, + { title: '2', content: 'content2', value: 1, status: 'finish' as const }, + { title: '3', content: 'content3', value: 2 }, + ]; + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items[0]).toHaveClass('t-steps-item--error'); + expect(items[1]).toHaveClass('t-steps-item--finish'); + expect(items[2]).toHaveClass('t-steps-item--wait'); + }); + }); + + describe('defaultCurrent 属性', () => { + test('非受控模式 defaultCurrent - options模式', async () => { + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items[1]).toHaveClass('t-steps-item--process'); + }); + + test('非受控模式 defaultCurrent - children模式', async () => { + const { getByTestId } = render( +
+ + + + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items[1]).toHaveClass('t-steps-item--process'); + }); + }); }); - test('layout vertical 测试', async () => { - const testId = 'step layout test'; + describe('交互行为', () => { + describe('点击交互', () => { + describe('theme=default', () => { + test('数字点击切换 - options模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + + const numbers = root.querySelectorAll('.t-steps-item__icon--number'); + expect(numbers[0]).toHaveTextContent('1'); - const { getByTestId } = render( -
- -
, - ); + // 初始状态 + const items = root.querySelectorAll('.t-steps-item'); + expect(items[0]).toHaveClass('t-steps-item--process'); - const stepsInstance = await waitFor(() => getByTestId(testId)); - const stepsItems = stepsInstance.querySelectorAll('.t-steps--vertical'); - expect(stepsItems.length).toBe(1); + // 点击第二个 + fireEvent.click(numbers[1]); + expect(items[0]).toHaveClass('t-steps-item--finish'); + expect(items[1]).toHaveClass('t-steps-item--process'); + }); + + test('数字点击切换 - children模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + + const items = root.querySelectorAll('.t-steps-item'); + expect(items.length).toBe(3); + + const numbers = root.querySelectorAll('.t-steps-item__icon--number'); + expect(numbers.length).toBe(3); + expect(numbers[0]).toHaveTextContent('1'); + + // 切换到第二个 + fireEvent.click(numbers[1]); + expect(items[0]).toHaveClass('t-steps-item--finish'); + expect(items[1]).toHaveClass('t-steps-item--process'); + }); + }); + + describe('theme=dot', () => { + test('图标点击切换 - options模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + + const numbers = root.querySelectorAll('.t-steps-item__icon--number'); + expect(numbers.length).toBe(0); + + const icons = root.querySelectorAll('.t-steps-item__icon'); + fireEvent.click(icons[1]); + const items = root.querySelectorAll('.t-steps-item'); + expect(items[1]).toHaveClass('t-steps-item--process'); + }); + + test('图标点击切换 - children模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + + const numbers = root.querySelectorAll('.t-steps-item__icon--number'); + expect(numbers.length).toBe(0); + + const icons = root.querySelectorAll('.t-steps-item__icon'); + fireEvent.click(icons[1]); + const items = root.querySelectorAll('.t-steps-item'); + expect(items[1]).toHaveClass('t-steps-item--process'); + }); + }); + + describe('content 区域点击', () => { + test('content 区域点击 - options模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + + const contents = root.querySelectorAll('.t-steps-item__content'); + fireEvent.click(contents[2]); + const items = root.querySelectorAll('.t-steps-item'); + expect(items[2]).toHaveClass('t-steps-item--process'); + }); + + test('content 区域点击 - children模式', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + + const contents = root.querySelectorAll('.t-steps-item__content'); + fireEvent.click(contents[2]); + const items = root.querySelectorAll('.t-steps-item'); + expect(items[2]).toHaveClass('t-steps-item--process'); + }); + }); + + describe('readonly 状态', () => { + test('readonly 时点击无效 - options模式', async () => { + const onChange = vi.fn(); + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + fireEvent.click(root.querySelector('.t-steps-item__icon')); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('readonly 时点击无效 - children模式', async () => { + const onChange = vi.fn(); + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + fireEvent.click(root.querySelector('.t-steps-item__icon')); + expect(onChange).not.toHaveBeenCalled(); + }); + }); + }); + + describe('状态变化', () => { + describe('onChange 回调', () => { + test('onChange 回调参数正确 - options模式', async () => { + const onChange = vi.fn(); + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + fireEvent.click(root.querySelector('.t-steps-item__inner')); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + test('onChange 回调参数正确 - children模式', async () => { + const onChange = vi.fn(); + const { getByTestId } = render( +
+ + + + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + fireEvent.click(root.querySelector('.t-steps-item__inner')); + expect(onChange).toHaveBeenCalledTimes(1); + }); + }); + + describe('非数值 value 映射', () => { + test('非数值 value 映射 - options模式', async () => { + const opts = [ + { title: 'a', content: 'a', value: 'a' }, + { title: 'b', content: 'b', value: 'b' }, + { title: 'c', content: 'c', value: 'c' }, + ]; + const onChange = vi.fn(); + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const icons = root.querySelectorAll('.t-steps-item__icon'); + fireEvent.click(icons[2]); + expect(onChange).toHaveBeenCalled(); + }); + + test('非数值 value 映射 - children模式', async () => { + const onChange = vi.fn(); + const { getByTestId } = render( +
+ + + + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const icons = root.querySelectorAll('.t-steps-item__icon'); + fireEvent.click(icons[2]); + expect(onChange).toHaveBeenCalled(); + }); + }); + }); }); - test('layout readonly 测试', async () => { - const testId = 'step readonly test'; + describe('特殊场景', () => { + describe('边缘情况', () => { + test('空 options', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items.length).toBe(0); + }); + + test('空 children', async () => { + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items.length).toBe(0); + }); + + test('单个步骤 - options模式', async () => { + const singleOption = [{ title: 'single', content: 'single content', value: 0 }]; + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items.length).toBe(1); + expect(items[0]).toHaveClass('t-steps-item--process'); + }); + + test('单个步骤 - children模式', async () => { + const { getByTestId } = render( +
+ + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items.length).toBe(1); + expect(items[0]).toHaveClass('t-steps-item--process'); + }); + + test('重复 value 映射 - options模式', async () => { + const opts = [ + { title: '1', content: 'content1', value: 0 }, + { title: '2', content: 'content2', value: 0 }, // 重复 value,后面的覆盖前面的 + { title: '3', content: 'content3', value: 1 }, + ]; + const { getByTestId } = render( +
+ +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items.length).toBe(3); + // 由于 value 重复,后面的覆盖前面的,所以 current=0 匹配第二个项目 + expect(items[0]).toHaveClass('t-steps-item--finish'); + expect(items[1]).toHaveClass('t-steps-item--process'); + expect(items[2]).toHaveClass('t-steps-item--wait'); + }); + }); + + describe('sequence 行为', () => { + describe('positive 顺序', () => { + test('positive 顺序 - options模式', async () => { + const onChange = vi.fn(); + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const icons = root.querySelectorAll('.t-steps-item__icon'); + expect(icons[0]).toHaveClass('t-steps-item-process'); + fireEvent.click(icons[2]); + expect(onChange.mock.calls[0][0]).toBe(2); + expect(onChange.mock.calls[0][1]).toBe(0); + }); + + test('positive 顺序 - children模式', async () => { + const onChange = vi.fn(); + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const icons = root.querySelectorAll('.t-steps-item__icon'); + expect(icons[0]).toHaveClass('t-steps-item-process'); + fireEvent.click(icons[2]); + expect(onChange.mock.calls[0][0]).toBe(2); + expect(onChange.mock.calls[0][1]).toBe(0); + }); + }); + + describe('reverse 倒序', () => { + test('reverse 倒序 - options模式', async () => { + const onChange = vi.fn(); + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const icons = root.querySelectorAll('.t-steps-item__icon'); + expect(icons[2]).toHaveClass('t-steps-item-process'); + fireEvent.click(icons[0]); + expect(onChange.mock.calls[0][0]).toBe(2); + expect(onChange.mock.calls[0][1]).toBe(0); + expect(icons[2]).toHaveClass('t-steps-item-finish'); + expect(icons[0]).toHaveClass('t-steps-item-process'); + }); + + test('reverse 倒序 - children模式', async () => { + const onChange = vi.fn(); + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const icons = root.querySelectorAll('.t-steps-item__icon'); + expect(icons[2]).toHaveClass('t-steps-item-process'); + fireEvent.click(icons[0]); + expect(onChange.mock.calls[0][0]).toBe(2); + expect(onChange.mock.calls[0][1]).toBe(0); + expect(icons[2]).toHaveClass('t-steps-item-finish'); + expect(icons[0]).toHaveClass('t-steps-item-process'); + }); + }); + }); + + describe('配置组合', () => { + const sequenceValues = ['positive', 'reverse'] as const; + const layoutValues = ['horizontal', 'vertical'] as const; + const themeValues = ['default', 'dot'] as const; + + const optionCombos: ReadonlyArray = sequenceValues.flatMap((s) => + layoutValues.flatMap((l) => themeValues.map((t) => [s, l, t] as const)), + ); + + test.each(optionCombos)('options模式: sequence=%s layout=%s theme=%s', async (sequence, layout, theme) => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + await clickEachAndVerify(root, sequence); + }); + + const slotSequenceThemeCombos: ReadonlyArray = [ + ['positive', 'default'], + ['reverse', 'default'], + ['positive', 'dot'], + ['reverse', 'dot'], + ]; + + test.each(slotSequenceThemeCombos)('children模式: sequence=%s theme=%s', async (sequence, theme) => { + const onChange = vi.fn(); + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const icons = root.querySelectorAll('.t-steps-item__icon'); + expect(icons.length).toBe(3); + await clickEachAndVerify(root, sequence); + expect(onChange).toHaveBeenCalled(); + }); + }); + }); - const { getByTestId } = render( -
- -
, - ); + describe('模式对比', () => { + describe('Options模式特性', () => { + test('options 数据驱动渲染', async () => { + const { getByTestId } = render(); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const titles = root.querySelectorAll('.t-steps-item__title'); + expect(titles.length).toBe(3); + expect(titles[0]).toHaveTextContent('0'); + }); + }); - const stepsInstance = await waitFor(() => getByTestId(testId)); - const stepsItems = stepsInstance.querySelectorAll('.t-steps-item--clickable'); - expect(stepsItems.length).toBe(0); + describe('Children模式特性', () => { + test('children 模式下 value 映射', async () => { + const { getByTestId } = render( +
+ + + + + +
, + ); + const root = await waitFor(() => getByTestId(TEST_ROOT_ID)); + const items = root.querySelectorAll('.t-steps-item'); + expect(items.length).toBe(3); + expect(items[0]).toHaveClass('t-steps-item--finish'); + expect(items[1]).toHaveClass('t-steps-item--process'); + expect(items[2]).toHaveClass('t-steps-item--wait'); + }); + }); }); }); diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index c2cf0ddd76..9a0b417eb4 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -96318,13 +96318,13 @@ exports[`csr snapshot test > csr test packages/components/steps/_example/vertica
csr test packages/components/steps/_example/vertica
csr test packages/components/steps/_example/vertica
csr test packages/components/steps/_example/vertica
- - - - - + 2
ssr test packages/components/steps/_example/sequenc exports[`ssr snapshot test > ssr test packages/components/steps/_example/status.tsx 1`] = `"
已完成的步骤
这里是提示文字
2
进行中的步骤
这里是提示文字
3
未进行的步骤
这里是提示文字
4
未进行的步骤
这里是提示文字
已完成的步骤
这里是提示文字
2
进行中的步骤
这里是提示文字
错误的步骤
优先展示\`t-step\`中设置的 status
4
未进行的步骤
这里是提示文字
"`; -exports[`ssr snapshot test > ssr test packages/components/steps/_example/vertical-no-sequence.tsx 1`] = `"
已完成的步骤
这里是提示文字
进行中的步骤
这里是提示文字
未进行的步骤
这里是提示文字
未进行的步骤
这里是提示文字
未进行的步骤
这里是提示文字
进行中的步骤
这里是提示文字
已完成的步骤
这里是提示文字
已完成的步骤
这里是提示文字
"`; +exports[`ssr snapshot test > ssr test packages/components/steps/_example/vertical-no-sequence.tsx 1`] = `"
已完成的步骤
这里是提示文字
进行中的步骤
这里是提示文字
未进行的步骤
这里是提示文字
未进行的步骤
这里是提示文字
未进行的步骤
这里是提示文字
进行中的步骤
这里是提示文字
已完成的步骤
这里是提示文字
已完成的步骤
这里是提示文字
"`; -exports[`ssr snapshot test > ssr test packages/components/steps/_example/vertical-sequence.tsx 1`] = `"
已完成的步骤
这里是提示文字
2
进行中的步骤
这里是提示文字
3
未进行的步骤
这里是提示文字
4
未进行的步骤
这里是提示文字
4
未进行的步骤
这里是提示文字
3
进行中的步骤
这里是提示文字
已完成的步骤
这里是提示文字
已完成的步骤
这里是提示文字
"`; +exports[`ssr snapshot test > ssr test packages/components/steps/_example/vertical-sequence.tsx 1`] = `"
已完成的步骤
这里是提示文字
2
进行中的步骤
这里是提示文字
3
未进行的步骤
这里是提示文字
4
未进行的步骤
这里是提示文字
4
未进行的步骤
这里是提示文字
3
进行中的步骤
这里是提示文字
2
已完成的步骤
这里是提示文字
已完成的步骤
这里是提示文字
"`; exports[`ssr snapshot test > ssr test packages/components/sticky-tool/_example/base.tsx 1`] = `"
chat
add
qrcode
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index 0543b94473..f9afa92788 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -944,9 +944,9 @@ exports[`ssr snapshot test > ssr test packages/components/steps/_example/sequenc exports[`ssr snapshot test > ssr test packages/components/steps/_example/status.tsx 1`] = `"
已完成的步骤
这里是提示文字
2
进行中的步骤
这里是提示文字
3
未进行的步骤
这里是提示文字
4
未进行的步骤
这里是提示文字
已完成的步骤
这里是提示文字
2
进行中的步骤
这里是提示文字
错误的步骤
优先展示\`t-step\`中设置的 status
4
未进行的步骤
这里是提示文字
"`; -exports[`ssr snapshot test > ssr test packages/components/steps/_example/vertical-no-sequence.tsx 1`] = `"
已完成的步骤
这里是提示文字
进行中的步骤
这里是提示文字
未进行的步骤
这里是提示文字
未进行的步骤
这里是提示文字
未进行的步骤
这里是提示文字
进行中的步骤
这里是提示文字
已完成的步骤
这里是提示文字
已完成的步骤
这里是提示文字
"`; +exports[`ssr snapshot test > ssr test packages/components/steps/_example/vertical-no-sequence.tsx 1`] = `"
已完成的步骤
这里是提示文字
进行中的步骤
这里是提示文字
未进行的步骤
这里是提示文字
未进行的步骤
这里是提示文字
未进行的步骤
这里是提示文字
进行中的步骤
这里是提示文字
已完成的步骤
这里是提示文字
已完成的步骤
这里是提示文字
"`; -exports[`ssr snapshot test > ssr test packages/components/steps/_example/vertical-sequence.tsx 1`] = `"
已完成的步骤
这里是提示文字
2
进行中的步骤
这里是提示文字
3
未进行的步骤
这里是提示文字
4
未进行的步骤
这里是提示文字
4
未进行的步骤
这里是提示文字
3
进行中的步骤
这里是提示文字
已完成的步骤
这里是提示文字
已完成的步骤
这里是提示文字
"`; +exports[`ssr snapshot test > ssr test packages/components/steps/_example/vertical-sequence.tsx 1`] = `"
已完成的步骤
这里是提示文字
2
进行中的步骤
这里是提示文字
3
未进行的步骤
这里是提示文字
4
未进行的步骤
这里是提示文字
4
未进行的步骤
这里是提示文字
3
进行中的步骤
这里是提示文字
2
已完成的步骤
这里是提示文字
已完成的步骤
这里是提示文字
"`; exports[`ssr snapshot test > ssr test packages/components/sticky-tool/_example/base.tsx 1`] = `"
chat
add
qrcode
"`;