Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/packages/switch/__test__/switch.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react'
import { render, fireEvent, waitFor, act } from '@testing-library/react'
import '@testing-library/jest-dom'
import { Loading1 } from '@nutui/icons-react'
import { Switch } from '../switch'

test('activeText && checked && onChange && inactiveText && className && style test', async () => {
Expand Down Expand Up @@ -49,8 +50,32 @@ test('activeText && checked && onChange && inactiveText && className && style te
}
})

test('disabled test', () => {
render(<Switch disabled />)
test('disabled test', async () => {
const { container } = render(<Switch disabled />)
const el = document.getElementsByClassName('nut-switch-disabled')
expect(el.length > 0).toBe(true)
const buttonEl: Element | null = container.querySelector('.nut-switch-button')
if (buttonEl) {
await act(() => {
fireEvent.click(buttonEl)
})
}
})

test('loadingIcon test', async () => {
const testFn = vi.fn()
const { container } = render(
<Switch loadingIcon={<Loading1 />} onChange={testFn} />
)
const el: Element | null = container.querySelector('.nut-switch-button')
if (el) {
await act(() => {
fireEvent.click(el)
})
waitFor(() => {
// 异步
const el = document.getElementsByClassName('.nut-icon')
expect(el.length > 0).toBe(true)
})
}
})
Comment on lines +65 to 81
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

loadingIcon测试中存在选择器错误,且缺少必要的断言

在此测试中发现以下问题:

  1. 第77行选择器.nut-icon中的前导点号会导致选择失败,应该使用nut-icon
  2. waitFor函数没有被await,可能导致测试时序问题
  3. 测试缺少验证onChange回调是否被调用的断言

建议修改如下:

test('loadingIcon test', async () => {
  const testFn = vi.fn()
  const { container } = render(
    <Switch loadingIcon={<Loading1 />} onChange={testFn} />
  )
  const el: Element | null = container.querySelector('.nut-switch-button')
  if (el) {
    await act(() => {
      fireEvent.click(el)
    })
-    waitFor(() => {
+    await waitFor(() => {
      // 异步
-      const el = document.getElementsByClassName('.nut-icon')
+      const iconEl = document.getElementsByClassName('nut-icon')
-      expect(el.length > 0).toBe(true)
+      expect(iconEl.length > 0).toBe(true)
+      expect(testFn).toHaveBeenCalled()
    })
  }
})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('loadingIcon test', async () => {
const testFn = vi.fn()
const { container } = render(
<Switch loadingIcon={<Loading1 />} onChange={testFn} />
)
const el: Element | null = container.querySelector('.nut-switch-button')
if (el) {
await act(() => {
fireEvent.click(el)
})
waitFor(() => {
// 异步
const el = document.getElementsByClassName('.nut-icon')
expect(el.length > 0).toBe(true)
})
}
})
test('loadingIcon test', async () => {
const testFn = vi.fn()
const { container } = render(
<Switch loadingIcon={<Loading1 />} onChange={testFn} />
)
const el: Element | null = container.querySelector('.nut-switch-button')
if (el) {
await act(() => {
fireEvent.click(el)
})
await waitFor(() => {
// 异步
const iconEl = document.getElementsByClassName('nut-icon')
expect(iconEl.length > 0).toBe(true)
expect(testFn).toHaveBeenCalled()
})
}
})

17 changes: 12 additions & 5 deletions src/packages/switch/demos/h5/demo2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@ import { Cell, Switch, Toast } from '@nutui/nutui-react'
const Demo2 = () => {
const [checkedAsync, setCheckedAsync] = useState(true)

const onChangeAsync = (value: boolean, event: any) => {
const mockRequest = (): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 2000)
})
}

const onChangeAsync = async (value: boolean) => {
Toast.show(`2秒后异步触发 ${value}`)
setTimeout(() => {
setCheckedAsync(value)
}, 2000)
await mockRequest()
setCheckedAsync(value)
}
return (
<Cell>
<Switch
checked={checkedAsync}
onChange={(value, event) => onChangeAsync(value, event)}
onChange={(value) => onChangeAsync(value)}
/>
</Cell>
)
Expand Down
16 changes: 11 additions & 5 deletions src/packages/switch/demos/taro/demo2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@ const Demo2 = () => {
const [checkedAsync, setCheckedAsync] = useState(true)
const [value, setValue] = useState(false)
const [showToast, setShowToast] = useState(false)
const mockRequest = (): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 2000)
})
}

const onChangeAsync = (value: boolean, event: any) => {
const onChangeAsync = async (value: boolean) => {
setValue(value)
setShowToast(true)
setTimeout(() => {
setCheckedAsync(value)
}, 2000)
await mockRequest()
setCheckedAsync(value)
}
return (
<>
<Cell>
<Switch
checked={checkedAsync}
onChange={(value, event) => onChangeAsync(value, event)}
onChange={(value) => onChangeAsync(value)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

更新事件处理器,匹配新的函数签名

更新了onChange属性的回调函数,现在只传递value参数给onChangeAsync函数,符合新的函数签名。

在异步操作期间(2秒延迟),应添加加载状态指示器以提升用户体验。建议在Switch组件中添加一个loading属性,并在状态更新前将其设置为true:

+  const [loading, setLoading] = useState(false)

  const onChangeAsync = (value: boolean) => {
    setValue(value)
    setShowToast(true)
+   setLoading(true)
    setTimeout(() => {
      setCheckedAsync(value)
+     setLoading(false)
    }, 2000)
  }

  return (
    <>
      <Cell>
        <Switch
          checked={checkedAsync}
+         loading={loading}
          onChange={(value) => onChangeAsync(value)}
        />
      </Cell>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onChange={(value) => onChangeAsync(value)}
import React, { useState } from 'react';
const Demo2 = () => {
const [value, setValue] = useState(false);
const [checkedAsync, setCheckedAsync] = useState(false);
const [showToast, setShowToast] = useState(false);
const [loading, setLoading] = useState(false);
const onChangeAsync = (value: boolean) => {
setValue(value);
setShowToast(true);
setLoading(true);
setTimeout(() => {
setCheckedAsync(value);
setLoading(false);
}, 2000);
};
return (
<>
<Cell>
<Switch
checked={checkedAsync}
loading={loading}
onChange={(value) => onChangeAsync(value)}
/>
</Cell>
</>
);
};
export default Demo2;

/>
</Cell>
<Toast
Expand Down
10 changes: 2 additions & 8 deletions src/packages/switch/demos/taro/demo6.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,14 @@ import { Cell, Switch, Toast } from '@nutui/nutui-react-taro'
const Demo6 = () => {
const [value, setValue] = useState(false)
const [showToast, setShowToast] = useState(false)
const onChange = (
value: boolean,
event: React.MouseEvent<Element, MouseEvent>
) => {
const onChange = (value: boolean) => {
setValue(value)
setShowToast(true)
}
return (
<>
<Cell>
<Switch
defaultChecked
onChange={(value, event) => onChange(value, event)}
/>
<Switch defaultChecked onChange={(value) => onChange(value)} />
</Cell>
<Toast
content={`触发了onChange事件,开关状态:${value}`}
Expand Down
1 change: 1 addition & 0 deletions src/packages/switch/doc.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { Switch } from '@nutui/nutui-react'
| disabled | Disabled | `boolean` | `false` |
| activeText | Text description when opening | `ReactNode` | `-` |
| inactiveText | Text description when closed | `ReactNode` | `-` |
| loadingIcon | Controlled loading state icon | `ReactNode` | `<Loading1 />` |
| onChange | Trigger when switching switches | `onChange:(value: boolean, event: Event)` | `-` |

## Theming
Expand Down
1 change: 1 addition & 0 deletions src/packages/switch/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { Switch } from '@nutui/nutui-react'
| disabled | 禁用状态 | `boolean` | `false` |
| activeText | 打开时文字描述 | `ReactNode` | `-` |
| inactiveText | 关闭时文字描述 | `ReactNode` | `-` |
| loadingIcon | 受控 loading 态图标 | `ReactNode` | `<Loading1 />` |
| onChange | 切换开关时触发 | `onChange:(value: boolean, event: Event)` | `-` |

## 主题定制
Expand Down
1 change: 1 addition & 0 deletions src/packages/switch/doc.taro.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { Switch } from '@nutui/nutui-react-taro'
| disabled | 禁用状态 | `boolean` | `false` |
| activeText | 打开时文字描述 | `ReactNode` | `-` |
| inactiveText | 关闭时文字描述 | `ReactNode` | `-` |
| loadingIcon | 受控 loading 态图标 | `ReactNode` | `<Loading1 />` |
| onChange | 切换开关时触发 | `onChange:(value: boolean, event: Event)` | `-` |

## 主题定制
Expand Down
1 change: 1 addition & 0 deletions src/packages/switch/doc.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { Switch } from '@nutui/nutui-react'
| disabled | 禁用狀態 | `boolean` | `false` |
| activeText | 打開時文字描述 | `ReactNode` | `-` |
| inactiveText | 關閉時文字描述 | `ReactNode` | `-` |
| loadingIcon | 受控 loading 態圖標 | `ReactNode` | `<Loading1 />` |
| onChange | 切換開關時觸發 | `onChange:(value: boolean, event: Event)` | `-` |

## 主題定製
Expand Down
5 changes: 5 additions & 0 deletions src/packages/switch/switch.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
left: calc(100% - $switch-height + $switch-border-width);
}
}
.nut-icon {
width: calc(($switch-height - $switch-border-width * 2) / 2);
height: calc(($switch-height - $switch-border-width * 2) / 2);
color: $switch-active-disabled-background-color;
}
}

&-close {
Expand Down
41 changes: 26 additions & 15 deletions src/packages/switch/switch.taro.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { FunctionComponent } from 'react'
import React, { FunctionComponent, useEffect, useState } from 'react'

import { ITouchEvent, View } from '@tarojs/components'
import { View } from '@tarojs/components'
import classNames from 'classnames'
import { Loading1 } from '@nutui/icons-react-taro'
import { ComponentDefaults } from '@/utils/typings'
import { usePropsValue } from '@/hooks/use-props-value'
import { useRtl } from '@/packages/configprovider/index.taro'
Expand All @@ -12,6 +13,7 @@ const defaultProps = {
disabled: false,
activeText: '',
inactiveText: '',
loadingIcon: <Loading1 />,
} as TaroSwitchProps
export const Switch: FunctionComponent<Partial<TaroSwitchProps>> = (props) => {
const {
Expand All @@ -20,6 +22,7 @@ export const Switch: FunctionComponent<Partial<TaroSwitchProps>> = (props) => {
disabled,
activeText,
inactiveText,
loadingIcon,
className,
style,
onChange,
Expand All @@ -37,6 +40,12 @@ export const Switch: FunctionComponent<Partial<TaroSwitchProps>> = (props) => {
defaultValue: defaultChecked,
})

useEffect(() => {
changing && setChanging(false)
}, [value])

const [changing, setChanging] = useState(false)

const classes = () => {
return classNames([
classPrefix,
Expand All @@ -49,20 +58,16 @@ export const Switch: FunctionComponent<Partial<TaroSwitchProps>> = (props) => {
])
}

const onClick = (
event: React.MouseEvent<Element, MouseEvent> | ITouchEvent
) => {
if (disabled) return
onChange && onChange(!value, event)
const onClick = () => {
if (disabled || changing) return
if (props.onChange) {
setChanging(true)
props.onChange(!value)
}
setValue(!value)
}
return (
<View
className={classes()}
onClick={(e) => onClick(e)}
style={style}
{...rest}
>
<View className={classes()} onClick={onClick} style={style} {...rest}>
<View
className={classNames([
[`${classPrefix}-button`],
Expand All @@ -77,8 +82,14 @@ export const Switch: FunctionComponent<Partial<TaroSwitchProps>> = (props) => {
},
])}
>
{!value && !activeText && (
<View className={`${classPrefix}-close-line`} />
{changing && loadingIcon ? (
<>{loadingIcon}</>
) : (
<>
{!value && !activeText && (
<View className={`${classPrefix}-close-line`} />
)}
</>
)}
</View>
{activeText && (
Expand Down
37 changes: 25 additions & 12 deletions src/packages/switch/switch.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { FunctionComponent } from 'react'
import React, { FunctionComponent, useEffect, useState } from 'react'
import classNames from 'classnames'
import { Loading1 } from '@nutui/icons-react'
import { ComponentDefaults } from '@/utils/typings'
import { usePropsValue } from '@/hooks/use-props-value'
import { useRtl } from '@/packages/configprovider'
Expand All @@ -10,6 +11,7 @@ const defaultProps = {
disabled: false,
activeText: '',
inactiveText: '',
loadingIcon: <Loading1 />,
} as WebSwitchProps
export const Switch: FunctionComponent<Partial<WebSwitchProps>> = (props) => {
const {
Expand All @@ -18,6 +20,7 @@ export const Switch: FunctionComponent<Partial<WebSwitchProps>> = (props) => {
disabled,
activeText,
inactiveText,
loadingIcon,
className,
style,
onChange,
Expand All @@ -35,6 +38,12 @@ export const Switch: FunctionComponent<Partial<WebSwitchProps>> = (props) => {
defaultValue: defaultChecked,
})

useEffect(() => {
changing && setChanging(false)
}, [value])

const [changing, setChanging] = useState(false)
Comment on lines +41 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

添加状态重置逻辑和状态变量

这段代码添加了两个关键功能:

  1. 使用 useEffectvalue 变化时重置 changing 状态
  2. 引入 changing 状态来跟踪组件的异步变化过程

这是处理异步状态变化的良好实践,但需确保测试覆盖。


🏁 Script executed:

#!/bin/bash
# 检查是否存在测试文件并验证是否覆盖了新增的状态逻辑
echo "查找 Switch 组件的测试文件..."
fd "switch.*test" --type f

Length of output: 79


代码状态变化逻辑已正确引入,但缺少相应测试覆盖

当前修改在 src/packages/switch/switch.tsx 中完成了以下两项关键变更:

  • 使用 useEffect 监听 value 的变化,并在变化时自动将 changing 状态重置为 false
  • 新增 changing 状态变量来追踪组件异步变化过程。

这些改动整体逻辑清晰且符合异步状态处理的良好实践。不过,通过执行检测脚本,目前没有找到针对 Switch 组件新增逻辑的测试文件。请确认或新增相应的单元测试,确保覆盖以下方面:

  • value 变化时,changing 状态能正确重置;
  • 异步状态变化过程中的其它相关交互行为。


const classes = () => {
return classNames([
classPrefix,
Expand All @@ -47,18 +56,16 @@ export const Switch: FunctionComponent<Partial<WebSwitchProps>> = (props) => {
])
}

const onClick = (event: React.MouseEvent<Element, MouseEvent>) => {
if (disabled) return
onChange && onChange(!value, event)
const onClick = () => {
if (disabled || changing) return
if (props.onChange) {
setChanging(true)
props.onChange(!value)
}
setValue(!value)
}
Comment on lines +60 to 66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

需要添加测试覆盖新增功能

静态分析工具显示这些关键的新增代码行没有测试覆盖,特别是:

  1. 异步加载状态的处理逻辑
  2. 加载图标的渲染

建议增加测试用例确保这些功能在各种条件下正常工作,例如:

  • 测试点击后的 changing 状态变化
  • 测试 value 更新后 changing 状态的重置
  • 测试加载图标的正确渲染
  • 测试在 changing 状态下点击不触发 onChange

这些测试对确保组件的稳定性和可靠性非常重要。

Also applies to: 84-84


🏁 Script executed:

#!/bin/bash
# 查找与 Switch 组件相关的测试文件和测试覆盖情况
echo "检查 Switch 组件的测试文件..."
fd "switch.*test" --type f

echo "检查测试中是否已经测试了异步处理逻辑..."
if fd "switch.*test" --type f | xargs rg -l "changing|loading|async"; then
  echo "找到了可能包含异步处理逻辑测试的文件"
else
  echo "未找到包含异步处理逻辑测试的文件"
fi

Length of output: 13008


请完善测试覆盖异步状态和加载图标功能

目前静态分析工具提示,新增的代码逻辑缺少相应的测试验证,建议对以下情况补充测试用例:

  • 点击事件后 changing 状态的变化:验证点击后是否正确设置 changing 状态,以及在 value 更新后该状态是否能重置。
  • 加载图标的渲染:确保在异步加载状态下加载图标能够正确显示。
  • changing 状态下的点击行为:测试当组件处于变化中(changingtrue)时,点击是否会被正确忽略,以防止重复触发 props.onChange

此外,这些问题同样适用于文件中其它局部相似的代码(例如:行 84)。请参考现有的 Switch 组件测试文件(如 src/packages/switch/__test__/switch.spec.tsx)补充对应的测试用例,以提高组件的稳定性和可靠性。

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 60-64: src/packages/switch/switch.tsx#L60-L64
Added lines #L60 - L64 were not covered by tests

return (
<div
className={classes()}
onClick={(e) => onClick(e)}
style={style}
{...rest}
>
<div className={classes()} onClick={onClick} style={style} {...rest}>
<div
className={classNames([
[`${classPrefix}-button`],
Expand All @@ -73,8 +80,14 @@ export const Switch: FunctionComponent<Partial<WebSwitchProps>> = (props) => {
},
])}
>
{!value && !activeText && (
<div className={`${classPrefix}-close-line`} />
{changing && loadingIcon ? (
<>{loadingIcon}</>
) : (
<>
{!value && !activeText && (
<div className={`${classPrefix}-close-line`} />
)}
</>
)}
</div>
{activeText && (
Expand Down
1 change: 1 addition & 0 deletions src/sites/sites-react/doc/docs/react/migrate-from-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ plugins: [

- `activeText` 属性类型更改为`ReactNode`
- `inactiveText` 属性类型更改为 `ReactNode`
- 新增 `loadingIcon` 属性,受控 loading 态图标

[//]: # '#### Toast'

Expand Down
1 change: 1 addition & 0 deletions src/sites/sites-react/doc/docs/taro/migrate-from-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ plugins: [

- `activeText` 属性类型更改为`ReactNode`
- `inactiveText` 属性类型更改为 `ReactNode`
- 新增 `loadingIcon` 属性,受控 loading 态图标

[//]: # '#### Toast'

Expand Down
5 changes: 3 additions & 2 deletions src/types/spec/switch/base.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ReactNode } from 'react'
import { BaseProps } from '../../base/props'

export interface BaseSwitch<EVENT = any> extends BaseProps {
export interface BaseSwitch extends BaseProps {
checked: boolean
defaultChecked: boolean
disabled: boolean
activeText: ReactNode
inactiveText: ReactNode
onChange: (val: boolean, event: EVENT) => void
loadingIcon: ReactNode
onChange: (val: boolean) => void
}
2 changes: 1 addition & 1 deletion src/types/spec/switch/h5.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { BaseSwitch } from '../switch/base'

export interface WebSwitchProps extends BaseSwitch<React.MouseEvent> {}
export interface WebSwitchProps extends BaseSwitch {}
4 changes: 1 addition & 3 deletions src/types/spec/switch/taro.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { ITouchEvent } from '@tarojs/components'
import { BaseSwitch } from './base'

type UnionEvent = React.MouseEvent<Element, MouseEvent> | ITouchEvent
export interface TaroSwitchProps extends BaseSwitch<UnionEvent> {}
export interface TaroSwitchProps extends BaseSwitch {}
Loading