Skip to content
Open
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@
},
"dependencies": {
"@babel/runtime": "^7.23.9",
"@nutui/icons-react": "^3.0.2-beta.5",
"@nutui/icons-react-taro": "^3.0.2-beta.3",
"@nutui/icons-react": "^3.0.2-beta.6",
"@nutui/icons-react-taro": "3.0.2-beta.6",
Comment on lines +110 to +111

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

版本锁定策略不一致

@nutui/icons-react 保持范围符 ^3.0.2-beta.6(允许小版本更新),而 @nutui/icons-react-taro 改为精确版本 3.0.2-beta.6(移除了 ^)。

这样的不一致可能导致 H5 和 Taro 版本的图标依赖在不同环境中出现版本分歧。请说明为什么需要这样的差异,或统一为相同的版本策略。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 110 - 111, The version specification strategy for
`@nutui/icons-react` and `@nutui/icons-react-taro` is inconsistent:
`@nutui/icons-react` uses a caret range (^3.0.2-beta.6) allowing minor version
updates, while `@nutui/icons-react-taro` uses an exact version (3.0.2-beta.6) with
the caret removed. Align these two icon package dependencies by applying the
same versioning strategy to both, either by adding the caret prefix to
`@nutui/icons-react-taro` to match `@nutui/icons-react`, or removing the caret from
`@nutui/icons-react` to match `@nutui/icons-react-taro`, ensuring consistency across
H5 and Taro icon dependencies.

"@nutui/jdesign-icons-react-taro": "1.0.6-beta.2",
"@nutui/lottie-miniprogram": "^1.0.2",
"@nutui/touch-emulator": "^1.0.0",
Expand Down
193 changes: 155 additions & 38 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@
"v15": 1,
"author": "lzz",
"dd": true,
"v16": false
"v16": true
},
{
"version": "3.0.0",
Expand Down
4 changes: 1 addition & 3 deletions src/packages/button/button.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,7 @@ export const Button = React.forwardRef<
onClick={(e) => handleClick(e as any)}
>
<View className="nut-button-wrap">
{loading && (
<Loading className="nut-icon-loading" ariaHidden aria-hidden />
)}
{loading && <Loading className="nut-icon-loading" aria-hidden="true" />}
{!loading && icon}
{children && (
<View
Expand Down
12 changes: 12 additions & 0 deletions src/packages/configprovider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,18 @@ export type NutCSSVariables =
| 'nutuiPopoverTextColor'
| 'nutuiPopoverDisableColor'
| 'nutuiPopoverDividerColor'
| 'nutuiPopoverPaddingHorizontal'
| 'nutuiPopoverPaddingVertical'
| 'nutuiPopoverHeight'
| 'nutuiPopoverIconSize'
| 'nutuiPopoverIconColor'
| 'nutuiPopoverStatusMaxWidth'
| 'nutuiPopoverDescriptionMaxWidth'
| 'nutuiPopoverActionHotspotSize'
| 'nutuiPopoverLightContentBackgroundColor'
| 'nutuiPopoverLightTextColor'
| 'nutuiPopoverLightIconColor'
| 'nutuiPopoverLightDividerColor'
| 'nutuiPopoverPadding'
| 'nutuiPopoverItemWidth'
| 'nutuiProgressHeight'
Expand Down
72 changes: 55 additions & 17 deletions src/packages/popover/__tests__/popover.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ test('render popover content', async () => {

test('render popover content dark', async () => {
const { container } = render(
<Popover visible list={itemListOne} theme="dark" location="right">
<Popover visible list={itemListOne} location="right">
<Button type="primary">基础用法</Button>
</Popover>
)
const content = document.querySelectorAll('.nut-popover')[0]
expect(content.className).toContain('nut-popover-dark')
expect(content.className).toContain('nut-popover--status')
expect(container).toMatchSnapshot()
})

Expand All @@ -95,30 +95,37 @@ test('render popover position with arrowOffset', async () => {
</Popover>
)

const checkArrowStyles = (location: FullPosition, expectedStyles: string) => {
const checkArrowStyles = (
location: FullPosition,
expectedStyles: string | null
) => {
rerender(
<Popover visible list={itemList} location={location} arrowOffset={20}>
<Button type="primary">基础用法</Button>
</Popover>
)
content = document.querySelectorAll('.nut-popover-arrow')[0]
expect(content).toHaveAttribute('style', expectedStyles)
if (expectedStyles === null) {
expect(content.getAttribute('style')).toBeNull()
} else {
expect(content).toHaveAttribute('style', expectedStyles)
}
}

let content = document.querySelectorAll('.nut-popover-arrow')[0]
expect(content).toHaveAttribute('style', 'left: 36px;')

checkArrowStyles('bottom', 'left: calc(50% + 20px);')
checkArrowStyles('bottom-right', 'right: -4px;')
checkArrowStyles('left', 'top: calc(50% - 20px);')
checkArrowStyles('left-bottom', 'bottom: 36px;')
checkArrowStyles('left-top', 'top: -4px;')
checkArrowStyles('right', 'top: calc(50% - 20px);')
checkArrowStyles('right-bottom', 'bottom: 36px;')
checkArrowStyles('right-top', 'top: -4px;')
checkArrowStyles('top-right', 'right: -4px;')
checkArrowStyles('top-left', 'left: 36px;')
checkArrowStyles('top', 'left: calc(50% + 20px);')
expect(content).toHaveAttribute('style', 'left: 20px;')

checkArrowStyles('bottom', null)
checkArrowStyles('bottom-right', 'right: 20px;')
checkArrowStyles('left', null)
checkArrowStyles('left-bottom', null)
checkArrowStyles('left-top', null)
checkArrowStyles('right', null)
checkArrowStyles('right-bottom', null)
checkArrowStyles('right-top', null)
checkArrowStyles('top-right', 'right: 20px;')
checkArrowStyles('top-left', 'left: 20px;')
checkArrowStyles('top', null)
})

test('render position fixed ', async () => {
Expand Down Expand Up @@ -187,6 +194,37 @@ test('should not emit select event when the action is disabled', async () => {
await waitFor(() => expect(choose).not.toBeCalled())
})

test('should auto show on mount when autoShow is true', () => {
const open = vi.fn()
render(
<Popover autoShow visible={false} list={itemList} onOpen={open}>
<Button type="primary">自动弹出</Button>
</Popover>
)
expect(open).toBeCalled()
})

test('should auto close after duration', () => {
vi.useFakeTimers()
const close = vi.fn()
const click = vi.fn()
render(
<Popover
visible
duration={5000}
list={itemList}
onClose={close}
onClick={click}
>
<Button type="primary">自动关闭</Button>
</Popover>
)
vi.advanceTimersByTime(5000)
expect(close).toBeCalled()
expect(click).toBeCalled()
vi.useRealTimers()
})
Comment on lines +207 to +226

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the test file structure
cat -n src/packages/popover/__tests__/popover.spec.tsx | sed -n '207,226p'

Repository: jdf2e/nutui-react

Length of output: 1917


🏁 Script executed:

# Also check the Popover component implementation to understand how duration works
fd -t f "popover" --type f -e ts -e tsx src/packages/popover/ | grep -v "spec\|test" | head -5

Repository: jdf2e/nutui-react

Length of output: 1917


🏁 Script executed:

# Let's look at the broader test file context
wc -l src/packages/popover/__tests__/popover.spec.tsx

Repository: jdf2e/nutui-react

Length of output: 1917


🌐 Web query:

Vitest vi.useFakeTimers try-finally cleanup pattern best practices

💡 Result:

Best-practice cleanup pattern for Vitest fake timers (vi.useFakeTimers) is: always restore real timers in a finally/afterEach, and clear any scheduled timers before switching back. Recommended pattern (per test) try { vi.useFakeTimers //...test code that schedules timers // optionally: vi.advanceTimersByTime(...), vi.runAllTimers, etc. } finally { vi.clearAllTimers vi.useRealTimers } Why this pattern 1) vi.useFakeTimers keeps faking timers until you call vi.useRealTimers. [1] 2) vi.clearAllTimers “removes all timers that are scheduled to run” so they will never run later (prevents leakage into subsequent tests). [1] 3) vi.useRealTimers restores mocked timers back to their original implementations; timers scheduled before switching back are discarded. [2] Notes / variations - If you only need a subset of timer APIs, pass options like vi.useFakeTimers({ toFake: [...] }) or vi.useFakeTimers({ toNotFake: [...] }). [1] - Dates: with fake timers, Date.now is mocked, and to control Date you can use vi.setSystemTime; when done you should restore with vi.useRealTimers in afterEach/cleanup. [3] Example using beforeEach/afterEach instead of per-test try/finally beforeEach( => { vi.useFakeTimers }) afterEach( => { vi.clearAllTimers vi.useRealTimers }) This is consistent with Vitest’s timer APIs: useFakeTimers wraps timer calls until useRealTimers, and clearAllTimers drops queued timers so they don’t affect later tests. [1][2]

Citations:


🌐 Web query:

React Testing Library act() wrapping vi.advanceTimersByTime Vitest integration

💡 Result:

When using Vitest fake timers with React Testing Library (RTL), you should generally wrap calls to vi.advanceTimersByTime in act [1][2][3]. This ensures that any React state updates triggered by the timer advancement are processed and flushed, preventing "not wrapped in act" warnings and ensuring your assertions reflect the updated component state [2][3]. Why act is required React processes state updates in batches [4]. When you manually advance timers using vi.advanceTimersByTime, the callbacks associated with those timers (such as setTimeout or setInterval) execute outside of React's standard update cycle [3]. Without act, React may not be aware of the state updates triggered by these callbacks, leading to warnings or race conditions where your assertions run before the component has finished re-rendering [2][3]. Recommended Usage Pattern To correctly advance timers and update the component, wrap the call in act: import { act, render, screen } from '@testing-library/react'; import { vi } from 'vitest'; test('advances timer correctly', => { vi.useFakeTimers; render(); act( => { vi.advanceTimersByTime(1000); }); expect(screen.getByText('Updated')).toBeInTheDocument; }); Handling user-event with Fake Timers If you use user-event in combination with fake timers, it is important to configure it to use your test runner's timer functions [5][6]. This prevents compatibility issues and test timeouts [6]: const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime.bind(vi), }); Best Practices Always restore timers: Use afterEach to clear timers and restore real timers to prevent side effects in other tests [6]. import { vi } from 'vitest'; afterEach( => { vi.runOnlyPendingTimers; vi.useRealTimers; }); Warning about "not wrapped in act": If you continue to see act warnings despite wrapping, ensure that your environment is properly configured for React 18+ and that you are not running into issues with asynchronous timer flushing (where you might need vi.runAllTimersAsync instead) [7][8]. Additionally, some newer versions of testing libraries are working toward better integration to minimize the need for manual act wrapping [9].

Citations:


在 Line 207-226 增强定时器用例隔离,避免测试串扰

当前测试缺少两个关键的最佳实践:

  1. 定时器清理不可靠vi.useRealTimers() 仅在成功路径执行;若断言提前失败,伪计时器会污染后续测试。
  2. 状态更新未包裹 act()vi.advanceTimersByTime() 触发的 React 状态更新需要在 act() 中执行,否则可能出现"not wrapped in act"警告或竞态条件。

建议使用 try-finally 结构并在 finally 块中同时调用 vi.clearAllTimers()vi.useRealTimers() 来完全清理定时器。

建议修改
 test('should auto close after duration', () => {
   vi.useFakeTimers()
   const close = vi.fn()
   const click = vi.fn()
-  render(
-    <Popover
-      visible
-      duration={5000}
-      list={itemList}
-      onClose={close}
-      onClick={click}
-    >
-      <Button type="primary">自动关闭</Button>
-    </Popover>
-  )
-  vi.advanceTimersByTime(5000)
-  expect(close).toBeCalled()
-  expect(click).toBeCalled()
-  vi.useRealTimers()
+  try {
+    render(
+      <Popover
+        visible
+        duration={5000}
+        list={itemList}
+        onClose={close}
+        onClick={click}
+      >
+        <Button type="primary">自动关闭</Button>
+      </Popover>
+    )
+    act(() => {
+      vi.advanceTimersByTime(5000)
+    })
+    expect(close).toBeCalled()
+    expect(click).toBeCalled()
+  } finally {
+    vi.clearAllTimers()
+    vi.useRealTimers()
+  }
 })
📝 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('should auto close after duration', () => {
vi.useFakeTimers()
const close = vi.fn()
const click = vi.fn()
render(
<Popover
visible
duration={5000}
list={itemList}
onClose={close}
onClick={click}
>
<Button type="primary">自动关闭</Button>
</Popover>
)
vi.advanceTimersByTime(5000)
expect(close).toBeCalled()
expect(click).toBeCalled()
vi.useRealTimers()
})
test('should auto close after duration', () => {
vi.useFakeTimers()
const close = vi.fn()
const click = vi.fn()
try {
render(
<Popover
visible
duration={5000}
list={itemList}
onClose={close}
onClick={click}
>
<Button type="primary">自动关闭</Button>
</Popover>
)
act(() => {
vi.advanceTimersByTime(5000)
})
expect(close).toBeCalled()
expect(click).toBeCalled()
} finally {
vi.clearAllTimers()
vi.useRealTimers()
}
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/packages/popover/__tests__/popover.spec.tsx` around lines 207 - 226, The
test "should auto close after duration" has two timer-related issues that need
fixing. First, add a try-finally block to ensure timers are properly cleaned up
even if assertions fail early; in the finally block, call both
vi.clearAllTimers() and vi.useRealTimers() to completely reset the timer state.
Second, wrap the vi.advanceTimersByTime() call with the act() function from
React testing library to properly handle React state updates triggered by timer
advancement, which prevents "not wrapped in act" warnings and potential race
conditions.


test('click event', async () => {
const close = vi.fn()
const close1 = vi.fn()
Expand Down
6 changes: 3 additions & 3 deletions src/packages/popover/demo.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ import Demo5 from './demos/taro/demo5'
const PopoverDemo = () => {
const [translated] = useTranslate({
'zh-CN': {
title: '基础用法',
title: '气泡类型',
title1: '选项配置',
title2: '自定义内容+颜色',
title3: '位置自定义:多条数据',
title6: '位置自定义:单条数据',
title4: '自定义目标元素',
},
'en-US': {
title: 'Basic Usage',
title: 'Bubble Types',
title1: 'Option Configuration',
title2: 'Custom Content and Color',
title3: 'Custom Location: multi datas',
title6: 'Custom Location: one data',
title4: 'Custom Target Element',
},
'zh-TW': {
title: '基礎用法',
title: '氣泡類型',
title1: '選項配置',
title2: '自定義內容+顏色',
title3: '位置自定義:多條資料',
Expand Down
6 changes: 3 additions & 3 deletions src/packages/popover/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Demo8 from './demos/h5/demo8'
const PopoverDemo = () => {
const [translated] = useTranslate({
'zh-CN': {
title: '基础用法',
title: '气泡类型',
title1: '选项配置',
title2: '自定义内容+颜色',
title3: '位置自定义:多条数据',
Expand All @@ -24,7 +24,7 @@ const PopoverDemo = () => {
fixed: '容器设置 position: fixed',
},
'en-US': {
title: 'Basic Usage',
title: 'Bubble Types',
title1: 'Option Configuration',
title2: 'Custom Content and Color',
title3: 'Custom Location: multi datas',
Expand All @@ -34,7 +34,7 @@ const PopoverDemo = () => {
fixed: 'position: fixed',
},
'zh-TW': {
title: '基礎用法',
title: '氣泡類型',
title1: '選項配置',
title2: '自定義內容+顏色',
title3: '位置自定義:多條資料',
Expand Down
66 changes: 50 additions & 16 deletions src/packages/popover/demos/h5/demo1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { Popover, Button, Space } from '@nutui/nutui-react'
import { Tips, Close } from '@nutui/icons-react'

const Demo = () => {
const [basic, setBasic] = useState(false)
const [dark, setDark] = useState(false)
const [index, setIndex] = useState(0)
const itemList = [
const [statusVisible, setStatusVisible] = useState(false)
const [descriptionVisible, setDescriptionVisible] = useState(false)
const [lightVisible, setLightVisible] = useState(false)
const [autoVisible, setAutoVisible] = useState(false)
const statusList = [
{
key: 'key1',
name: '主要文案内容',
Expand All @@ -15,42 +16,75 @@ const Demo = () => {
icon: <Close />,
onClick: (e: any) => {
e.stopPropagation()
index === 0 && basic && setBasic(false)
index === 1 && dark && setDark(false)
setStatusVisible(false)
},
},
},
]
const descriptionList = [
{
key: 'key1',
name: '主要文案内容',
},
]
return (
<Space>
<Popover
visible={basic}
list={itemList}
visible={statusVisible}
type="status"
list={statusList}
location="bottom-left"
onClick={() => {
basic ? setBasic(false) : setBasic(true)
setIndex(0)
statusVisible ? setStatusVisible(false) : setStatusVisible(true)
}}
onOpen={() => {
console.log('打开菜单时触发')
}}
onClose={() => {
console.log('关闭菜单时触发')
}}
>
<Button type="primary">状态型</Button>
</Popover>
<Popover
visible={descriptionVisible}
type="description"
list={descriptionList}
location="bottom-left"
onClick={() => {
descriptionVisible
? setDescriptionVisible(false)
: setDescriptionVisible(true)
}}
>
<Button type="primary">说明型</Button>
</Popover>
<Popover
visible={lightVisible}
type="status"
theme="light"
list={statusList}
location="bottom-left"
onClick={() => {
lightVisible ? setLightVisible(false) : setLightVisible(true)
}}
>
<Button type="primary">明亮风格</Button>
</Popover>
<Popover
visible={dark}
list={itemList}
theme="dark"
autoShow
duration={5000}
visible={autoVisible}
type="description"
list={descriptionList}
location="bottom-left"
onOpen={() => setAutoVisible(true)}
onClose={() => setAutoVisible(false)}
onClick={() => {
dark ? setDark(false) : setDark(true)
setIndex(1)
autoVisible ? setAutoVisible(false) : setAutoVisible(true)
}}
>
<Button type="primary">暗黑风格</Button>
<Button type="primary">自动弹出</Button>
</Popover>
</Space>
)
Expand Down
2 changes: 0 additions & 2 deletions src/packages/popover/demos/h5/demo2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ const Demo2 = () => {
<Space>
<Popover
visible={showIcon}
theme="dark"
location="bottom-left"
onClick={() => {
showIcon ? setShowIcon(false) : setShowIcon(true)
Expand All @@ -55,7 +54,6 @@ const Demo2 = () => {
</Popover>
<Popover
visible={disableAction}
theme="dark"
onClick={() => {
disableAction ? setDisableAction(false) : setDisableAction(true)
}}
Expand Down
2 changes: 1 addition & 1 deletion src/packages/popover/demos/h5/demo3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const Demo3 = () => {
display: 'flex',
} as any
const itemStyle = {
marginTop: '10px',
margin: '10px 0',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
Expand Down
1 change: 0 additions & 1 deletion src/packages/popover/demos/h5/demo4-1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ const Demo = () => {
</div>
</Picker>
<Popover
theme="dark"
visible={customPositon}
targetId="pickerTarget2"
list={positionList}
Expand Down
6 changes: 5 additions & 1 deletion src/packages/popover/demos/h5/demo4.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const Demo4 = () => {
key: 'key1',
name: '主要文案内容',
},
{
key: 'key2',
name: '主要文案内容',
},
]

const handlePicker = () => {
Expand Down Expand Up @@ -76,9 +80,9 @@ const Demo4 = () => {
</div>
</Picker>
<Popover
theme="dark"
visible={customPositon}
targetId="pickerTarget"
type="description"
list={positionList}
location={curPostion}
/>
Expand Down
1 change: 0 additions & 1 deletion src/packages/popover/demos/h5/demo5.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const Demo5 = () => {
return (
<>
<Popover
theme="dark"
visible={customTarget}
targetId="popid"
list={iconItemList}
Expand Down
1 change: 0 additions & 1 deletion src/packages/popover/demos/h5/demo7.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const Demo7 = () => {
<div style={{ height: 100 }} />
<Popover
className="demo-popover"
theme="dark"
visible={visible}
list={itemList}
location="top-left"
Expand Down
1 change: 0 additions & 1 deletion src/packages/popover/demos/h5/demo8.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const Demo8 = () => {
className="demo-popover"
visible={visible}
list={list}
theme="dark"
location="top-right"
style={{ marginInlineEnd: '30px' }}
closeOnOutsideClick={false}
Expand Down
Loading
Loading