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
9 changes: 5 additions & 4 deletions .agents/skills/mpx2rn/references/rn-api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1106,9 +1106,10 @@ mpx.use(apiProxy, {
| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `url` | `string` | 是 | `wss://` 或 `ws://` 地址。 |
| `header` | `Object` | 否 | 请求头,会传递给 RN 原生 **WebSocket**。 |
| `protocols` | `string[]` | 否 | 子协议列表。 |

> 微信同名 API 的 `header`、`timeout`、`tcpNoDelay`、`perMessageDeflate` 等扩展参数**当前 RN 中暂不支持**。
> 微信同名 API 的 `timeout`、`tcpNoDelay`、`perMessageDeflate` 等扩展参数**当前 RN 中暂不支持**。

#### 返回值

Expand Down Expand Up @@ -1576,13 +1577,13 @@ mpx.use(apiProxy, {

#### 说明

取消键盘高度监听:仅移除与注册时**同一引用**的 **`callback`**;**不传引用不会移除其它监听**。当监听列表为空时会移除键盘显示/隐藏相关订阅。
取消键盘高度监听:传入与注册时**同一引用**的 **`callback`** 时移除对应监听;不传回调时移除全部监听。当监听列表为空时会移除键盘显示/隐藏相关订阅。

#### 入参

| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `callback` | `function` | | 与 **`onKeyboardHeightChange`** 注册时为同一函数引用。 |
| `callback` | `function` | | 与 **`onKeyboardHeightChange`** 注册时为同一函数引用;不传时移除全部监听。 |

#### 返回值

Expand Down Expand Up @@ -1730,7 +1731,7 @@ mpx.config.rnConfig.bluetoothPermission = () => {
| `stopWifi` | 结束 Wi‑Fi 模块并清空列表监听;RN iOS 目标直接走 `fail`。 | RN Android 成功时为 `{ errMsg: 'stopWifi:ok' }`。 |
| `getWifiList` | `startWifi` 就绪后扫描热点,通过 `onGetWifiList` 交付列表;RN iOS 目标直接走 `fail`。 | `success`:`{ errMsg: 'getWifiList:ok', errno: 0, errCode: 0 }`;热点列表不在此载荷中。 |
| `onGetWifiList` | 注册接收热点列表的回调。 | `{ wifiList }`;每项含 `SSID`、`BSSID`、`frequency`、`signalStrength`。 |
| `offGetWifiList` | 移除热点列表回调。 | 传入与注册时同一 `callback`;当前实现不支持省略参数清空全部。 |
| `offGetWifiList` | 移除热点列表回调。 | 传入与注册时同一 `callback`;省略参数清空全部。 |
| `getConnectedWifi` | 读取当前已连接 Wi‑Fi;支持 `partialInfo`,且需先 `startWifi` 成功就绪。 | `{ errMsg: 'getConnectedWifi:ok', wifi }`;`wifi` 含 `SSID`、`BSSID`、`signalStrength`、`frequency`。 |

Wi-Fi API 的成功 `errMsg` 与微信文档保持一致,均以 `:ok` 结尾。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@

### 注意事项 {#notes}
InnerAudioContext 音频资源不会自动释放,因此如果不再需要使用音频,请及时调用 InnerAudioContext.destroy() 释放资源,避免内存泄漏。

Web 端的 `on*` 事件允许注册多个监听函数。调用对应的 `off*` 方法并传入注册时的同一函数引用,只移除该监听;不传参数时移除该事件的全部监听。
4 changes: 2 additions & 2 deletions docs-vitepress/api-proxy/network/websocket/connectSocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

创建一个 WebSocket 连接。

支持情况: 微信、支付宝
支持情况: 微信、支付宝、RN、web

[参考文档](https://developers.weixin.qq.com/miniprogram/dev/api/network/upload/wx.uploadFile.html)

Expand All @@ -13,7 +13,7 @@
| 属性 | 类型 | 默认值 | 必填 | 说明 | 最低版本 | 支付宝 | RN | web |
| ------------------- | -------------- | ------ | ---- | -------------------------------------------- | -------- | ------ | ---------- | --- |
| url | string | | 是 | 开发者服务器 wss 接口地址 | | <span style="color: green; font-weight: bold;">✓</span> | <span style="color: green; font-weight: bold;">✓</span> | <span style="color: green; font-weight: bold;">✓</span> |
| header | Object | | 否 | HTTP Header,Header 中不能设置 Referer | | <span style="color: green; font-weight: bold;">✓</span> | <span style="color: red; font-weight: bold;"></span> | <span style="color: red; font-weight: bold;">✗</span> |
| header | Object | | 否 | HTTP Header,Header 中不能设置 Referer | | <span style="color: green; font-weight: bold;">✓</span> | <span style="color: green; font-weight: bold;"></span> | <span style="color: red; font-weight: bold;">✗</span> |
| protocols | Array.&lt;string&gt; | | 否 | 子协议数组 | 1.4.0 | <span style="color: green; font-weight: bold;">✓</span> | <span style="color: green; font-weight: bold;">✓</span> | <span style="color: green; font-weight: bold;">✓</span> |
| tcpNoDelay | boolean | false | 否 | 建立 TCP 连接的时候的 TCP_NODELAY 设置 | 2.4.0 | <span style="color: red; font-weight: bold;">✗</span> | <span style="color: red; font-weight: bold;">✗</span> | <span style="color: red; font-weight: bold;">✗</span> |
| perMessageDeflate | boolean | false | 否 | 是否开启压缩扩展 | 2.8.0 | <span style="color: red; font-weight: bold;">✗</span> | <span style="color: red; font-weight: bold;">✗</span> | <span style="color: red; font-weight: bold;">✗</span> |
Expand Down
2 changes: 2 additions & 0 deletions docs-vitepress/api-proxy/storage/clearStorageSync.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

支持情况: 微信、支付宝、web

> RN 的存储实现基于异步的 AsyncStorage,不支持此同步 API,请使用 `mpx.clearStorage`。

[参考文档](https://developers.weixin.qq.com/miniprogram/dev/api/storage/wx.clearStorageSync.html)


Expand Down
2 changes: 2 additions & 0 deletions docs-vitepress/api-proxy/storage/removeStorageSync.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

支持情况: 微信、支付宝、web

> RN 的存储实现基于异步的 AsyncStorage,不支持此同步 API,请使用 `mpx.removeStorage`。

[参考文档](https://developers.weixin.qq.com/miniprogram/dev/api/storage/wx.removeStorageSync.html)


Expand Down
30 changes: 30 additions & 0 deletions packages/api-proxy/__tests__/ali/set-navigation-bar.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ENV_OBJ } from '../../src/common/js'
import { setNavigationBarTitle } from '../../src/platform/api/set-navigation-bar/index.ali'

jest.mock('../../src/common/js/index', () => {
const actual = jest.requireActual('../../src/common/js/index')
return Object.assign({}, actual, {
ENV_OBJ: {
canIUse: jest.fn(() => true),
setNavigationBarTitle: jest.fn()
}
})
})

describe('Ali setNavigationBarTitle', () => {
test('should normalize the success errMsg', () => {
const success = jest.fn()
ENV_OBJ.setNavigationBarTitle.mockImplementation(({ success }) => {
success({})
})

setNavigationBarTitle({
title: 'title',
success
})

expect(success).toHaveBeenCalledWith({
errMsg: 'setNavigationBarTitle:ok'
})
})
})
14 changes: 14 additions & 0 deletions packages/api-proxy/__tests__/rn/ble-connection.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,18 @@ describe('RN BLE connection APIs', () => {
}))
expect(BleManager.requestMTU).not.toHaveBeenCalled()
})

test.each([false, 0, ''])('generic parameter check should reject invalid value %p', (value) => {
const fail = jest.fn()
const complete = jest.fn()

getBLEDeviceRSSI({ deviceId: value, fail, complete })

expect(fail).toHaveBeenCalledWith({
errMsg: 'getBLEDeviceRSSI:fail parameter error',
errno: 1509000
})
expect(complete).toHaveBeenCalled()
expect(BleManager.readRSSI).not.toHaveBeenCalled()
})
})
32 changes: 32 additions & 0 deletions packages/api-proxy/__tests__/rn/camera.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import CameraContext from '../../src/platform/api/camera/rnCamera'

describe('RN CameraContext', () => {
test('setZoom should invoke fail only when the native method throws', () => {
const cameraContext = new CameraContext()
const success = jest.fn()
const fail = jest.fn()
const complete = jest.fn()
cameraContext.camera = {
setZoom: jest.fn(() => {
throw new Error('set zoom failed')
})
}

cameraContext.setZoom({
zoom: 2,
success,
fail,
complete
})

expect(cameraContext.camera.setZoom).toHaveBeenCalledWith(2)
expect(success).not.toHaveBeenCalled()
expect(fail).toHaveBeenCalledWith({
errMsg: 'setZoom:fail set zoom failed'
})
expect(complete).toHaveBeenCalledTimes(1)
expect(complete).toHaveBeenCalledWith({
errMsg: 'setZoom:fail set zoom failed'
})
})
})
92 changes: 92 additions & 0 deletions packages/api-proxy/__tests__/rn/keyboard.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const mockSubscriptions = []
const mockAddListener = jest.fn(eventName => {
// RN 新版 addListener 返回订阅对象,业务代码只能移除自己持有的订阅。
const subscription = {
eventName,
remove: jest.fn()
}
mockSubscriptions.push(subscription)
return subscription
})
const mockRemoveAllListeners = jest.fn()
const mockDismiss = jest.fn()

jest.mock('react-native', () => ({
Keyboard: {
addListener: mockAddListener,
removeAllListeners: mockRemoveAllListeners,
dismiss: mockDismiss
}
}), { virtual: true })

const {
onKeyboardHeightChange,
offKeyboardHeightChange,
hideKeyboard
} = require('../../src/platform/api/keyboard/index.ios')

describe('RN keyboard', () => {
beforeEach(() => {
offKeyboardHeightChange()
mockSubscriptions.length = 0
mockAddListener.mockClear()
mockRemoveAllListeners.mockClear()
mockDismiss.mockReset()
})

it('should only remove owned subscriptions when called without callback', () => {
onKeyboardHeightChange(jest.fn())

offKeyboardHeightChange()

// 不传回调会清空业务回调,但不能调用 removeAllListeners 误删外部监听。
expect(mockSubscriptions).toHaveLength(2)
mockSubscriptions.forEach(subscription => {
expect(subscription.remove).toHaveBeenCalledTimes(1)
})
expect(mockRemoveAllListeners).not.toHaveBeenCalled()
})

it('should only remove owned subscriptions when the last callback is removed', () => {
const callback = jest.fn()
onKeyboardHeightChange(callback)

offKeyboardHeightChange(callback)

// 最后一个业务回调移除后,键盘显示和隐藏两个底层订阅都应释放。
mockSubscriptions.forEach(subscription => {
expect(subscription.remove).toHaveBeenCalledTimes(1)
})
expect(mockRemoveAllListeners).not.toHaveBeenCalled()
})

it('should subscribe again after all callbacks are removed', () => {
const callback = jest.fn()
onKeyboardHeightChange(callback)
offKeyboardHeightChange(callback)

onKeyboardHeightChange(jest.fn())

// 首轮订阅释放后再次注册,需要重新创建两个底层订阅。
expect(mockAddListener).toHaveBeenCalledTimes(4)
})

it('should include the API name in the hideKeyboard fail errMsg', () => {
const fail = jest.fn()
const complete = jest.fn()
mockDismiss.mockImplementation(() => {
throw new Error('dismiss failed')
})

hideKeyboard({
fail,
complete
})

const result = {
errMsg: 'hideKeyboard:fail dismiss failed'
}
expect(fail).toHaveBeenCalledWith(result)
expect(complete).toHaveBeenCalledWith(result)
})
})
85 changes: 85 additions & 0 deletions packages/api-proxy/__tests__/rn/network.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const mockUnsubscribe = jest.fn()
const mockAddEventListener = jest.fn(() => mockUnsubscribe)

jest.mock('@react-native-community/netinfo', () => ({
__esModule: true,
default: {
addEventListener: mockAddEventListener,
fetch: jest.fn()
},
NetInfoStateType: {
cellular: 'cellular',
none: 'none',
wifi: 'wifi'
}
}), { virtual: true })

jest.mock('../../src/common/js', () => ({
successHandle: jest.fn(),
failHandle: jest.fn(),
defineUnsupportedProps: jest.fn()
}))

const {
getNetworkType,
offNetworkStatusChange,
onNetworkStatusChange
} = require('../../src/platform/api/device/network/rnNetwork')
const NetInfo = require('@react-native-community/netinfo').default
const { failHandle } = require('../../src/common/js')

describe('RN network events', () => {
beforeEach(() => {
offNetworkStatusChange()
mockAddEventListener.mockClear()
mockUnsubscribe.mockClear()
NetInfo.fetch.mockReset()
failHandle.mockClear()
})

test('should clear callbacks and native subscription when callback is null', () => {
onNetworkStatusChange(jest.fn())

// null 与 undefined 含义一致,都应清空回调并释放 NetInfo 底层订阅。
offNetworkStatusChange(null)

expect(mockUnsubscribe).toHaveBeenCalledTimes(1)
})

test('should release native subscription after the last callback is removed', () => {
const callbackA = jest.fn()
const callbackB = jest.fn()
onNetworkStatusChange(callbackA)
onNetworkStatusChange(callbackB)

offNetworkStatusChange(callbackA)
expect(mockUnsubscribe).not.toHaveBeenCalled()

offNetworkStatusChange(callbackB)
expect(mockUnsubscribe).toHaveBeenCalledTimes(1)
})

test('should subscribe again after all callbacks are removed', () => {
onNetworkStatusChange(jest.fn())
offNetworkStatusChange()

// 底层订阅释放后再次调用 on,需要重新向 NetInfo 注册。
onNetworkStatusChange(jest.fn())

expect(mockAddEventListener).toHaveBeenCalledTimes(2)
})

test('getNetworkType should include API name when fetching fails', async () => {
const fail = jest.fn()
const complete = jest.fn()
NetInfo.fetch.mockRejectedValue(new Error('fetch failed'))

getNetworkType({ fail, complete })
await Promise.resolve()
await Promise.resolve()

expect(failHandle).toHaveBeenCalledWith({
errMsg: 'getNetworkType:fail fetch failed'
}, fail, complete)
})
})
53 changes: 53 additions & 0 deletions packages/api-proxy/__tests__/rn/socket.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { connectSocket } from '../../src/platform/api/socket/index.ios'
import { connectSocket as connectSocketWeb } from '../../src/platform/api/socket/index.web'

class MockWebSocket {
constructor (url, protocols, options) {
this.url = url
this.protocols = protocols
this.options = options
this.readyState = 0
this.CONNECTING = 0
this.OPEN = 1
this.CLOSING = 2
this.CLOSED = 3
this.close = jest.fn()
}
}

describe('RN connectSocket', () => {
beforeEach(() => {
global.WebSocket = jest.fn((...args) => new MockWebSocket(...args))
})

it('should forward header and protocols to WebSocket', () => {
const header = {
Authorization: 'token'
}

connectSocket({
url: 'wss://example.com',
protocols: ['chat'],
header
})

expect(global.WebSocket).toHaveBeenCalledWith(
'wss://example.com',
['chat'],
{
headers: header
}
)
})

it('should not forward header options on web', () => {
connectSocketWeb({
url: 'wss://example.com',
header: {
Authorization: 'token'
}
})

expect(global.WebSocket).toHaveBeenCalledWith('wss://example.com')
})
})
Loading
Loading