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..6261fe1315
--- /dev/null
+++ b/packages/components/steps/__tests__/steps.branches.test.tsx
@@ -0,0 +1,57 @@
+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';
+
+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..5c07fae13e 100644
--- a/packages/components/steps/__tests__/steps.test.tsx
+++ b/packages/components/steps/__tests__/steps.test.tsx
@@ -1,86 +1,303 @@
-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(
-
-
-
-
-
- ,
- );
+const TEST_ROOT_ID = 'step-test-root';
- expect(() => {
- wrapper.unmount();
- }).not.toThrow();
- });
+const StepRender = (props: TdStepsProps) => {
+ const [current, setCurrent] = useState(0);
+ return (
+
+ {
+ setCurrent(c);
+ props?.onChange && props.onChange(c, p);
+ }}
+ />
+
+ );
+};
- test('options 测试', async () => {
- const testId = 'step options test';
- const handleChange = vi.fn();
+const SlotRender = (props: TdStepsProps & { initial?: number }) => {
+ const [current, setCurrent] = useState(props.initial ?? 0);
+ return (
+
+ {
+ setCurrent(c);
+ props?.onChange && props.onChange(c, p);
+ }}
+ >
+
+
+
+
+
+ );
+};
- const { getByTestId } = render(
-
-
-
,
- );
+// --- 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 stepsInstance = await waitFor(() => getByTestId(testId));
- const stepsItems = stepsInstance.querySelectorAll('.t-steps-item');
- expect(stepsItems.length).toBe(3);
+const findProcessingIndex = (items: Element[]) => items.findIndex((it) => nodeHasState(it, 'process'));
- fireEvent.click(stepsInstance.querySelector('.t-steps-item__inner'));
- expect(handleChange).toBeCalledTimes(1);
+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('options', () => {
+ test('mount/unmount', () => {
+ const wrapper = render();
+ expect(() => wrapper.unmount()).not.toThrow();
+ });
+
+ test('layout vertical', async () => {
+ const { getByTestId } = render();
+ const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
+ expect(root.querySelectorAll('.t-steps--vertical').length).toBe(1);
+ });
+
+ test('readonly 不可点击', async () => {
+ const { getByTestId } = render();
+ const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
+ expect(root.querySelectorAll('.t-steps-item--clickable').length).toBe(0);
+ });
+
+ describe('theme / behavior', () => {
+ test('theme=default 基本渲染与切换', 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 icons = root.querySelectorAll('.t-steps-item__icon');
+ expect(icons.length).toBe(3);
+
+ const numbers = root.querySelectorAll('.t-steps-item__icon--number');
+ expect(numbers.length).toBe(3);
+ expect(numbers[0]).toHaveTextContent('1');
+
+ const titles = root.querySelectorAll('.t-steps-item__title');
+ expect(titles[0]).toHaveTextContent('0');
- test('layout vertical 测试', async () => {
- const testId = 'step layout test';
+ // 初始状态: first process, others wait
+ expect(items[0]).toHaveClass('t-steps-item--process');
+ expect(items[1]).toHaveClass('t-steps-item--wait');
- const { getByTestId } = render(
-
-
-
,
+ // 切换到第二个
+ fireEvent.click(numbers[1]);
+ expect(items[0]).toHaveClass('t-steps-item--finish');
+ expect(items[1]).toHaveClass('t-steps-item--process');
+
+ // 切换到第三个(通过 content 区域)
+ const contents = root.querySelectorAll('.t-steps-item__content');
+ fireEvent.click(contents[2]);
+ expect(items[2]).toHaveClass('t-steps-item--process');
+ expect(items[1]).toHaveClass('t-steps-item--finish');
+ });
+
+ test('theme=dot 不渲染数字,切换行为', 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('@change called with correct args (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);
+ });
+ });
+
+ describe('sequence 行为 (options)', () => {
+ test('positive 正序', 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[1]);
+ expect(onChange.mock.calls[0][0]).toBe(1);
+ expect(onChange.mock.calls[0][1]).toBe(0);
+ });
+
+ test('reverse 倒序', async () => {
+ const onChange = vi.fn();
+ const { getByTestId } = render();
+ const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
+ const icons = root.querySelectorAll('.t-steps-item__icon');
+ // 初始倒序:最后一个应为 finish/process 视实现而定,此处断言为 finish
+ expect(icons[2]).toHaveClass('t-steps-item-finish');
+ fireEvent.click(icons[0]);
+ expect(onChange.mock.calls[0][0]).toBe(2);
+ });
+ });
+
+ // broader combinations and edge cases for options-based rendering
+ 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)(
+ '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 stepsInstance = await waitFor(() => getByTestId(testId));
- const stepsItems = stepsInstance.querySelectorAll('.t-steps--vertical');
- expect(stepsItems.length).toBe(1);
+ 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('readonly 时点击不触发 onChange', 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('非数值 value 正确映射与切换', 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');
+ expect(icons.length).toBe(3);
+ fireEvent.click(icons[2]);
+ expect(onChange).toHaveBeenCalled();
+ });
});
- test('layout readonly 测试', async () => {
- const testId = 'step readonly test';
+ describe('slot children', () => {
+ test('slot 渲染与自定义 icon', 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');
+ });
- const { getByTestId } = render(
-
-
-
,
- );
+ test('slot @change called and status change', 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).toHaveBeenCalledTimes(1);
+ });
+
+ const slotSequenceThemeCombos: ReadonlyArray = [
+ ['positive', 'default'],
+ ['reverse', 'default'],
+ ['positive', 'dot'],
+ ['reverse', 'dot'],
+ ];
- const stepsInstance = await waitFor(() => getByTestId(testId));
- const stepsItems = stepsInstance.querySelectorAll('.t-steps-item--clickable');
- expect(stepsItems.length).toBe(0);
+ test.each(slotSequenceThemeCombos)('sequence=%s theme=%s (slot)', 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();
+ });
});
});