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
24 changes: 24 additions & 0 deletions packages/taro-components-react/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ process.env.TARO_ENV = 'h5'
process.env.TARO_PLATFORM = 'web'
process.env.SUPPORT_TARO_POLYFILL = 'disabled'

// Mock Taro API
const mockGetSystemInfo = jest.fn(({ success, fail } = { success: undefined, fail: undefined }) => {
try {
success?.({
windowWidth: 375,
windowHeight: 667,
pixelRatio: 2,
lengthScaleRatio: 1
})
} catch (error) {
fail?.(error)
}
})

jest.mock('@tarojs/taro', () => {
return {
__esModule: true,
default: {
getSystemInfo: mockGetSystemInfo,
},
getSystemInfo: mockGetSystemInfo,
}
})
Comment on lines +10 to +32
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for getSystemInfo usage patterns in taro-components-react package
echo "=== getSystemInfo usage in taro-components-react ==="
rg -n "getSystemInfo" packages/taro-components-react -C3

echo ""
echo "=== Other @tarojs/taro imports/usage in taro-components-react ==="
rg -n "@tarojs/taro|from.*taro" packages/taro-components-react --type ts --type tsx -C1

Repository: NervJS/taro

Length of output: 2054


🏁 Script executed:

#!/bin/bash
# Search for other @tarojs/taro imports in source files
echo "=== @tarojs/taro imports in src ==="
rg -n "from.*@tarojs/taro|import.*@tarojs/taro" packages/taro-components-react/src -C1

echo ""
echo "=== @tarojs/taro imports in tests ==="
rg -n "from.*@tarojs/taro|import.*@tarojs/taro" packages/taro-components-react/__tests__ -C1

echo ""
echo "=== All Taro.* API calls in src ==="
rg -n "Taro\." packages/taro-components-react/src -C1

Repository: NervJS/taro

Length of output: 2153


🏁 Script executed:

#!/bin/bash
# Check what test files exist and what they import
echo "=== Test files in __tests__ ==="
find packages/taro-components-react/__tests__ -name "*.ts" -o -name "*.tsx" | head -20

echo ""
echo "=== Content of setup.ts (full file) ==="
cat packages/taro-components-react/__tests__/setup.ts

echo ""
echo "=== Check if pull-down-refresh has tests ==="
find packages/taro-components-react/__tests__ -name "*pull*" -o -name "*refresh*"

Repository: NervJS/taro

Length of output: 15338


Verify the test impact and fix necessity with eventCenter mock

Based on verification, the review comment is partially correct but misfocused:

Real issue: The jest.mock completely replaces @tarojs/taro and omits Taro.eventCenter, which is used in pull-down-refresh/index.tsx (lines 130, 137). Currently low-impact because pull-down-refresh has no tests, but it's a latent risk.

Not an issue: mockGetSystemInfo only supports callback style, but code only uses callbacks (picker-group.tsx line 44). No Promise-style usage exists, so the defensive Promise wrapping suggestion is unnecessary.

The review should prioritize the incomplete mock risk over the hypothetical Promise concern. The suggested jest.requireActual approach is sound and recommended.


Taro.getSystemInfo mock 应确保完整 mock @tarojs/taro 模块中的其他 API

好主意把 Picker 依赖的系统信息提出来做全局 mock。但有一个需要尽早规避的潜在风险:

当前 jest.mock 只提供了 getSystemInfo,遗漏了其他 Taro API:

  • pull-down-refresh/index.tsx 中使用了 Taro.eventCenter.on()Taro.eventCenter.off()(第 130、137 行)
  • 虽然目前 pull-down-refresh 还没有对应的测试用例,但如果后续添加测试,这些 API 会是 undefined,导致测试失败
  • 建议使用 jest.requireActual 做部分 mock,将真实模块的内容透传,只覆盖需要 mock 的 getSystemInfo
jest.mock('@tarojs/taro', () => {
+  const actual = jest.requireActual('@tarojs/taro')
  return {
    __esModule: true,
+    ...actual,
-    default: {
+    default: {
+      ...actual.default,
       getSystemInfo: mockGetSystemInfo,
     },
     getSystemInfo: mockGetSystemInfo,
   }
})

这样既能保证 Picker 的系统信息 mock 生效,又不会破坏其他组件对 Taro API 的依赖。

🤖 Prompt for AI Agents
In packages/taro-components-react/__tests__/setup.ts around lines 10–32, the
current jest.mock fully replaces @tarojs/taro and only provides getSystemInfo,
which omits other runtime APIs like Taro.eventCenter used in pull-down-refresh
(lines 130,137) and creates a latent test break; update the mock to import the
real module via jest.requireActual and merge it with an override for
getSystemInfo (and ensure both the default export and named export are
preserved), or explicitly forward eventCenter (on/off) from the real module so
other components keep working while getSystemInfo remains mocked.


// Mock Taro 组件
// eslint-disable-next-line react/display-name
jest.mock('@tarojs/components', () => {
Expand Down
22 changes: 17 additions & 5 deletions packages/taro-components-react/src/components/picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export interface PickerColors {
cancelButtonColor?: string // 取消按钮颜色
itemDefaultColor?: string // 选项字体默认颜色
itemSelectedColor?: string // 选项字体选中颜色
backgroundColor?: string // 面板背景色
lineColor?: string // 指示线条颜色
titleColor?: string // 标题颜色
}

export interface PickerDate {
Expand Down Expand Up @@ -142,6 +145,7 @@ interface IProps {
forwardedRef?: React.MutableRefObject<HTMLDivElement | null>
formType?: string
lang?: string // 语言参数,支持 'zh-CN'、'en-US'、'en-GB'
theme?: 'light' | 'dark' // 主题模式,默认 'light'
}

interface IState {
Expand Down Expand Up @@ -190,6 +194,7 @@ const Picker = React.forwardRef<PickerRef, IProps>((props, ref) => {
children,
formType,
lang,
theme = 'light',
...restProps
} = props
const indexRef = React.useRef<number[]>([])
Expand Down Expand Up @@ -878,9 +883,11 @@ const Picker = React.forwardRef<PickerRef, IProps>((props, ref) => {
const clsMask = classNames('taro-picker__mask-overlay', 'taro-picker__animate-fade-in', {
'taro-picker__animate-fade-out': state.fadeOut
})
const clsSlider = classNames('taro-picker', 'taro-picker__animate-slide-up', {
const clsSlider = classNames('taro-picker', 'taro-picker__animate-slide-up', `taro-picker-theme-${theme}`, {
'taro-picker__animate-slide-down': state.fadeOut
})
const backgroundStyle = colors.backgroundColor ? { backgroundColor: colors.backgroundColor } : null
const titleStyle = colors.titleColor ? { color: colors.titleColor } : null

// 暴露方法给外部
React.useImperativeHandle(ref, () => ({
Expand All @@ -903,8 +910,8 @@ const Picker = React.forwardRef<PickerRef, IProps>((props, ref) => {
{!state.hidden && (
<View className="taro-picker__overlay">
<View className={clsMask} onClick={handleCancel} />
<View className={clsSlider}>
<View className="taro-picker__hd">
<View className={clsSlider} {...(backgroundStyle ? { style: backgroundStyle } : {})}>
<View className="taro-picker__hd" {...(backgroundStyle ? { style: backgroundStyle } : {})}>
<View
className="taro-picker__action"
onClick={handleCancel}
Expand All @@ -913,7 +920,12 @@ const Picker = React.forwardRef<PickerRef, IProps>((props, ref) => {
{textProps.cancelText ?? langText.cancel}
</View>
{headerText && (
<View className="taro-picker__title">{headerText}</View>
<View
className="taro-picker__title"
{...(titleStyle ? { style: titleStyle } : {})}
>
{headerText}
</View>
)}
<View
className="taro-picker__action"
Expand All @@ -923,7 +935,7 @@ const Picker = React.forwardRef<PickerRef, IProps>((props, ref) => {
{textProps.okText ?? langText.confirm}
</View>
</View>
<View className="taro-picker__bd">{renderPickerGroup}</View>
<View className="taro-picker__bd" {...(backgroundStyle ? { style: backgroundStyle } : {})}>{renderPickerGroup}</View>
</View>
</View>
)}
Expand Down
129 changes: 109 additions & 20 deletions packages/taro-components-react/src/components/picker/picker-group.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ScrollView, View } from '@tarojs/components'
import Taro from '@tarojs/taro'
import * as React from 'react'

// 添加类型定义
Expand All @@ -18,6 +19,7 @@ export interface PickerGroupProps {
colors?: {
itemDefaultColor?: string // 选项字体默认颜色
itemSelectedColor?: string // 选项字体选中颜色
lineColor?: string // 选中指示线颜色
}
}

Expand All @@ -26,6 +28,49 @@ const PICKER_LINE_HEIGHT = 34 // px
const PICKER_VISIBLE_ITEMS = 7 // 可见行数
const PICKER_BLANK_ITEMS = 3 // 空白行数

const getIndicatorStyle = (lineColor: string): React.CSSProperties => {
return {
borderTopColor: lineColor,
borderBottomColor: lineColor
}
}

// 辅助函数:获取系统信息的 lengthScaleRatio 并设置 targetScrollTop
const setTargetScrollTopWithScale = (
setTargetScrollTop: (value: number) => void,
baseValue: number,
randomOffset?: number
) => {
Taro.getSystemInfo({
success: (res) => {
let lengthScaleRatio = (res as any)?.lengthScaleRatio
if (lengthScaleRatio == null || lengthScaleRatio === 0) {
console.warn('Taro.getSystemInfo: lengthScaleRatio 不存在,使用计算值')
lengthScaleRatio = 1
if (res.windowWidth < 320) {
lengthScaleRatio = res.windowWidth / 320
} else if (res.windowWidth >= 400 && res.windowWidth < 600) {
lengthScaleRatio = res.windowWidth / 400
}
const shortSide = res.windowWidth < res.windowHeight ? res.windowWidth : res.windowHeight
const isBigScreen = shortSide >= 600
if (isBigScreen) {
lengthScaleRatio = shortSide / 720
}
}
const scaledValue = baseValue * lengthScaleRatio
const finalValue = randomOffset !== undefined ? scaledValue + randomOffset : scaledValue
setTargetScrollTop(finalValue)
},
fail: (err) => {
console.error('获取系统信息失败:', err)
// 失败时使用默认值 1
const finalValue = randomOffset !== undefined ? baseValue + randomOffset : baseValue
setTargetScrollTop(finalValue)
}
})
}

export function PickerGroupBasic(props: PickerGroupProps) {
const {
range = [],
Expand All @@ -36,6 +81,7 @@ export function PickerGroupBasic(props: PickerGroupProps) {
selectedIndex = 0, // 使用selectedIndex参数,默认为0
colors = {},
} = props
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null
const [targetScrollTop, setTargetScrollTop] = React.useState(0)
const scrollViewRef = React.useRef<TaroScrollView>(null)
const itemRefs = React.useRef<Array<TaroView | null>>([])
Expand All @@ -46,8 +92,12 @@ export function PickerGroupBasic(props: PickerGroupProps) {

const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT)
React.useEffect(() => {
if (scrollViewRef.current) {
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length
if (process.env.TARO_PLATFORM !== 'harmony') {
if (scrollViewRef.current && scrollViewRef.current?.scrollHeight) {
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length
} else {
console.warn('Height measurement anomaly')
}
}
}, [range.length]) // 只在range长度变化时重新计算
// 获取选中的索引
Expand All @@ -58,7 +108,8 @@ export function PickerGroupBasic(props: PickerGroupProps) {
// 当selectedIndex变化时,调整滚动位置
React.useEffect(() => {
if (scrollViewRef.current && range.length > 0 && !isTouching) {
setTargetScrollTop(selectedIndex * itemHeightRef.current)
const baseValue = selectedIndex * itemHeightRef.current
setTargetScrollTopWithScale(setTargetScrollTop, baseValue)
setCurrentIndex(selectedIndex)
}
}, [selectedIndex, range])
Expand All @@ -80,7 +131,9 @@ export function PickerGroupBasic(props: PickerGroupProps) {
const newIndex = getSelectedIndex(scrollTop)

setIsTouching(false)
setTargetScrollTop(newIndex * itemHeightRef.current + Math.random() * 0.001) // 随机数为了在一个项内滚动时强制刷新
const baseValue = newIndex * itemHeightRef.current
const randomOffset = Math.random() * 0.001 // 随机数为了在一个项内滚动时强制刷新
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset)
updateIndex(newIndex, columnId)
onColumnChange?.({ columnId, index: newIndex })
}, 100)
Expand Down Expand Up @@ -146,7 +199,10 @@ export function PickerGroupBasic(props: PickerGroupProps) {
return (
<View className="taro-picker__group">
<View className="taro-picker__mask" />
<View className="taro-picker__indicator" />
<View
className="taro-picker__indicator"
{...(indicatorStyle ? { style: indicatorStyle } : {})}
/>
<ScrollView
ref={scrollViewRef}
scrollY
Expand Down Expand Up @@ -178,6 +234,7 @@ export function PickerGroupTime(props: PickerGroupProps) {
colors = {},
} = props

const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null
const [targetScrollTop, setTargetScrollTop] = React.useState(0)
const scrollViewRef = React.useRef<TaroScrollView>(null)
const itemRefs = React.useRef<Array<TaroView | null>>([])
Expand All @@ -186,8 +243,12 @@ export function PickerGroupTime(props: PickerGroupProps) {

const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT)
React.useEffect(() => {
if (scrollViewRef.current) {
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length
if (process.env.TARO_PLATFORM !== 'harmony') {
if (scrollViewRef.current && scrollViewRef.current?.scrollHeight) {
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length
} else {
console.warn('Height measurement anomaly')
}
}
}, [range.length]) // 只在range长度变化时重新计算

Expand All @@ -198,7 +259,8 @@ export function PickerGroupTime(props: PickerGroupProps) {
// 当selectedIndex变化时,调整滚动位置
React.useEffect(() => {
if (scrollViewRef.current && range.length > 0 && !isTouching) {
setTargetScrollTop(selectedIndex * itemHeightRef.current)
const baseValue = selectedIndex * itemHeightRef.current
setTargetScrollTopWithScale(setTargetScrollTop, baseValue)
setCurrentIndex(selectedIndex)
}
}, [selectedIndex, range])
Expand All @@ -224,7 +286,9 @@ export function PickerGroupTime(props: PickerGroupProps) {
const isLimited = Boolean(updateIndex(newIndex, columnId, true))
// 如果没有触发限位,才执行归中逻辑
if (!isLimited) {
setTargetScrollTop(newIndex * itemHeightRef.current + Math.random() * 0.001)
const baseValue = newIndex * itemHeightRef.current
const randomOffset = Math.random() * 0.001
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset)
}
}, 100)
}
Expand Down Expand Up @@ -290,7 +354,10 @@ export function PickerGroupTime(props: PickerGroupProps) {
return (
<View className="taro-picker__group">
<View className="taro-picker__mask" />
<View className="taro-picker__indicator" />
<View
className="taro-picker__indicator"
{...(indicatorStyle ? { style: indicatorStyle } : {})}
/>
<ScrollView
ref={scrollViewRef}
scrollY
Expand Down Expand Up @@ -321,15 +388,20 @@ export function PickerGroupDate(props: PickerGroupProps) {
colors = {},
} = props

const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null
const [targetScrollTop, setTargetScrollTop] = React.useState(0)
const scrollViewRef = React.useRef<TaroScrollView>(null)
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex)
const [isTouching, setIsTouching] = React.useState(false)

const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT)
React.useEffect(() => {
if (scrollViewRef.current) {
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length
if (process.env.TARO_PLATFORM !== 'harmony') {
if (scrollViewRef.current && scrollViewRef.current?.scrollHeight) {
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length
} else {
console.warn('Height measurement anomaly')
}
}
}, [range.length]) // 只在range长度变化时重新计算

Expand All @@ -340,7 +412,8 @@ export function PickerGroupDate(props: PickerGroupProps) {
// 当selectedIndex变化时,调整滚动位置
React.useEffect(() => {
if (scrollViewRef.current && range.length > 0 && !isTouching) {
setTargetScrollTop(selectedIndex * itemHeightRef.current)
const baseValue = selectedIndex * itemHeightRef.current
setTargetScrollTopWithScale(setTargetScrollTop, baseValue)
setCurrentIndex(selectedIndex)
}
}, [selectedIndex, range])
Expand All @@ -364,7 +437,9 @@ export function PickerGroupDate(props: PickerGroupProps) {
const newIndex = getSelectedIndex(scrollTop)

setIsTouching(false)
setTargetScrollTop(newIndex * itemHeightRef.current + Math.random() * 0.001) // 随机数为了在一个项内滚动时强制刷新
const baseValue = newIndex * itemHeightRef.current
const randomOffset = Math.random() * 0.001 // 随机数为了在一个项内滚动时强制刷新
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset)

// 更新日期值
if (updateDay) {
Expand Down Expand Up @@ -434,7 +509,10 @@ export function PickerGroupDate(props: PickerGroupProps) {
return (
<View className="taro-picker__group">
<View className="taro-picker__mask" />
<View className="taro-picker__indicator" />
<View
className="taro-picker__indicator"
{...(indicatorStyle ? { style: indicatorStyle } : {})}
/>
<ScrollView
ref={scrollViewRef}
scrollY
Expand Down Expand Up @@ -464,6 +542,7 @@ export function PickerGroupRegion(props: PickerGroupProps) {
colors = {},
} = props

const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null
const scrollViewRef = React.useRef<any>(null)
const [targetScrollTop, setTargetScrollTop] = React.useState(0)
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex)
Expand All @@ -472,8 +551,12 @@ export function PickerGroupRegion(props: PickerGroupProps) {
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT)
const isUserBeginScrollRef = React.useRef(false)
React.useEffect(() => {
if (scrollViewRef.current) {
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length
if (process.env.TARO_PLATFORM !== 'harmony') {
if (scrollViewRef.current && scrollViewRef.current?.scrollHeight) {
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length
} else {
console.warn('Height measurement anomaly')
}
}
}, [range.length]) // 只在range长度变化时重新计算

Expand All @@ -484,7 +567,8 @@ export function PickerGroupRegion(props: PickerGroupProps) {
// 当selectedIndex变化时,调整滚动位置
React.useEffect(() => {
if (scrollViewRef.current && range.length > 0 && !isTouching) {
setTargetScrollTop(selectedIndex * itemHeightRef.current)
const baseValue = selectedIndex * itemHeightRef.current
setTargetScrollTopWithScale(setTargetScrollTop, baseValue)
setCurrentIndex(selectedIndex)
}
}, [selectedIndex, range])
Expand All @@ -505,7 +589,9 @@ export function PickerGroupRegion(props: PickerGroupProps) {
const newIndex = getSelectedIndex(scrollTop)

setIsTouching(false)
setTargetScrollTop(newIndex * itemHeightRef.current + Math.random() * 0.001) // 随机数为了在一个项内滚动时强制刷新
const baseValue = newIndex * itemHeightRef.current
const randomOffset = Math.random() * 0.001 // 随机数为了在一个项内滚动时强制刷新
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset)
updateIndex(newIndex, columnId, false, isUserBeginScrollRef.current)
}, 100)
}
Expand Down Expand Up @@ -569,7 +655,10 @@ export function PickerGroupRegion(props: PickerGroupProps) {
return (
<View className="taro-picker__group">
<View className="taro-picker__mask" />
<View className="taro-picker__indicator" />
<View
className="taro-picker__indicator"
{...(indicatorStyle ? { style: indicatorStyle } : {})}
/>
<ScrollView
ref={scrollViewRef}
scrollY
Expand Down
Loading
Loading