@onekeyfe/react-native-native-list 6.7.0-alpha.398 → 6.7.0-alpha.400

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,25 @@ import XCTest
4
4
 
5
5
  @MainActor
6
6
  final class NativeListLifecycleTests: XCTestCase {
7
+ func testCanonicalNullPatchRemovesStoredOptionalFields() throws {
8
+ let view = NativeListView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
9
+ let row: [String: Any] = ["key": "token", "type": "identity", "title": "Token",
10
+ "leading": ["kind": "token"], "subtitle": "Old",
11
+ "titleImage": ["image": ["uri": "data:image/png;base64,AA==", "width": 14, "height": 14]]]
12
+ let snapshot: [String: Any] = ["schemaVersion": 1, "generation": 1,
13
+ "layout": ["kind": "linear"], "rows": [row]]
14
+ view.applySnapshotJson(String(decoding: try JSONSerialization.data(withJSONObject: snapshot), as: UTF8.self))
15
+ let patches: [[String: Any]] = [["key": "token", "type": "identity",
16
+ "changes": ["title": "New", "subtitle": NSNull(), "titleImage": NSNull()]]]
17
+ view.applyPatchesJson(String(decoding: try JSONSerialization.data(withJSONObject: patches), as: UTF8.self))
18
+ let items = try XCTUnwrap(Mirror(reflecting: view).descendant("itemsByKey") as? [String: NativeListItem])
19
+ let accepted = try XCTUnwrap(items["token"])
20
+ XCTAssertEqual(accepted.data.string("title"), "New")
21
+ XCTAssertNil(accepted.data["subtitle"])
22
+ XCTAssertNil(accepted.data["titleImage"])
23
+ XCTAssertEqual(accepted.key, "token")
24
+ }
25
+
7
26
  func testAcceptedRowIndexReusesStableShapeAndRejectsLateInvalidPatch() throws {
8
27
  let view = NativeListView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
9
28
  func snapshot(_ keys: [String], generation: Int, title: String = "Old") throws -> String {
@@ -229,6 +248,47 @@ final class NativeListLifecycleTests: XCTestCase {
229
248
  XCTAssertNotEqual(cell.bindingEpoch, changedEpoch)
230
249
  }
231
250
 
251
+ func testIdentityTitleImageRetainsRequestAndClearsOnProductionCellReuse() throws {
252
+ let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 320, height: 60))
253
+ let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
254
+ window.addSubview(cell)
255
+ func bind(_ descriptor: [String: Any]?, title: String = "STOCK") throws {
256
+ var data: [String: Any] = ["key": "stock", "type": "identity", "title": title,
257
+ "height": 60, "leading": ["kind": "token", "fallbackIcon": ["name": "CryptoCoinOutline"]]]
258
+ if let descriptor { data["titleImage"] = descriptor }
259
+ cell.bind(item: try NativeListItem(data: data), theme: nil, layout: "linear", selected: false,
260
+ checkboxState: { _, _, fallback in fallback }, fonts: NativeListFontFamilies())
261
+ cell.layoutIfNeeded()
262
+ }
263
+ let descriptor: [String: Any] = ["image": ["uri": "data:image/png;base64,AA==", "width": 14, "height": 14],
264
+ "accessibilityLabel": "Issuer", "hoverActionKey": "issuer-info"]
265
+ try bind(descriptor)
266
+ let slot = try XCTUnwrap(Mirror(reflecting: cell).children.first {
267
+ $0.label == "titleImageSlot"
268
+ }?.value as? NativeListImageSlot)
269
+ XCTAssertFalse(slot.view.isHidden)
270
+ XCTAssertEqual(slot.view.bounds.width, 14, accuracy: 0.5)
271
+ XCTAssertEqual(slot.view.bounds.height, 14, accuracy: 0.5)
272
+ XCTAssertEqual(slot.view.layer.cornerRadius, 7, accuracy: 0.1)
273
+ XCTAssertEqual(slot.view.accessibilityLabel, "Issuer")
274
+ let identity = Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String
275
+ XCTAssertNotNil(identity)
276
+ try bind(descriptor, title: "Changed title")
277
+ XCTAssertEqual(Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String, identity)
278
+ try bind(["image": ["uri": "data:image/png;base64,AQ==", "width": 20, "height": 20]])
279
+ XCTAssertEqual(slot.view.bounds.width, 20, accuracy: 0.5)
280
+ XCTAssertEqual(slot.view.layer.cornerRadius, 10, accuracy: 0.1)
281
+ XCTAssertNil(slot.view.accessibilityLabel)
282
+ XCTAssertNotEqual(Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String, identity)
283
+ try bind(nil)
284
+ XCTAssertTrue(slot.view.isHidden)
285
+ XCTAssertNil(Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String)
286
+ try bind(descriptor)
287
+ cell.prepareForReuse()
288
+ XCTAssertTrue(slot.view.isHidden)
289
+ XCTAssertNil(slot.view.accessibilityLabel)
290
+ }
291
+
232
292
  func testRiskIdentityTitleIconAndButtonResetOnProductionCellBind() throws {
233
293
  let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 320, height: 56))
234
294
  let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
@@ -1559,12 +1619,15 @@ final class NativeListLifecycleTests: XCTestCase {
1559
1619
  let thousandCells = try sampleCells(count: 1000)
1560
1620
  print("NativeList 100 visible/attached first/mid/tail: \(hundredCells)")
1561
1621
  print("NativeList 1000 visible/attached first/mid/tail: \(thousandCells)")
1622
+ let visibleCellLimit = Int(ceil(list.bounds.height / 60)) + 2
1562
1623
  XCTAssertTrue(hundredCells.allSatisfy {
1563
- $0.visible > 0 && $0.visible <= 24 && $0.attached > 0 && $0.attached <= 24
1624
+ $0.visible > 0 && $0.visible <= visibleCellLimit && $0.attached >= $0.visible
1564
1625
  })
1565
1626
  XCTAssertTrue(thousandCells.allSatisfy {
1566
- $0.visible > 0 && $0.visible <= 24 && $0.attached > 0 && $0.attached <= 24
1627
+ $0.visible > 0 && $0.visible <= visibleCellLimit && $0.attached >= $0.visible
1567
1628
  })
1629
+ // UIKit also retains offscreen cells in subviews. Their count may exceed
1630
+ // the visible budget, but must remain bounded as the data grows tenfold.
1568
1631
  XCTAssertLessThanOrEqual(thousandCells.map(\.attached).max() ?? 0,
1569
1632
  (hundredCells.map(\.attached).max() ?? 0) + 4)
1570
1633
  }
@@ -1881,6 +1944,7 @@ final class NativeListLifecycleTests: XCTestCase {
1881
1944
  for (index, slot) in slots.enumerated() { view.mountContainerSlot(slot, atIndex: index) }
1882
1945
  view.setContainerSlotHeightsJson("[0,0,140,0,100,40]")
1883
1946
  var settlements: [(String, String, Bool)] = []
1947
+ let latestSettled = expectation(description: "Latest disclosure transition settled")
1884
1948
  view.onStructuralTransitionSettled = { payload in
1885
1949
  guard let data = payload.data(using: .utf8),
1886
1950
  let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
@@ -1888,6 +1952,7 @@ final class NativeListLifecycleTests: XCTestCase {
1888
1952
  let status = object["status"] as? String,
1889
1953
  let expanded = object["settledExpanded"] as? Bool else { return }
1890
1954
  settlements.append((id, status, expanded))
1955
+ if id == "collapse-latest" { latestSettled.fulfill() }
1891
1956
  }
1892
1957
  view.applySnapshotJson(try structuralRowsSnapshot(count: 6, generation: 1,
1893
1958
  disclosureExpanded: false))
@@ -1908,11 +1973,15 @@ final class NativeListLifecycleTests: XCTestCase {
1908
1973
  view.applySnapshotJson(try structuralRowsSnapshot(count: 50, generation: 4,
1909
1974
  transitionID: "expand-superseded", durationMs: 400,
1910
1975
  disclosureExpanded: true))
1911
- waitForStructuralFrames(0.08)
1976
+ // Supersede on the same main-thread turn: a timed wait can outlast the
1977
+ // expansion on a busy simulator and legitimately deliver its settlement.
1978
+ XCTAssertNotNil(Mirror(reflecting: view).descendant("structuralDisplayLink")
1979
+ as? CADisplayLink)
1980
+ XCTAssertEqual(settlements.map(\.0), ["same-rows"])
1912
1981
  view.applySnapshotJson(try structuralRowsSnapshot(count: 6, generation: 5,
1913
1982
  transitionID: "collapse-latest", durationMs: 400,
1914
1983
  disclosureExpanded: false))
1915
- waitForStructuralFrames(0.5)
1984
+ wait(for: [latestSettled], timeout: 2)
1916
1985
  XCTAssertEqual(settlements.map(\.0), ["same-rows", "collapse-latest"])
1917
1986
  XCTAssertEqual(settlements.last?.1, "completed")
1918
1987
  XCTAssertEqual(settlements.last?.2, false)
@@ -4,7 +4,7 @@ import { useNativeListFontFamiliesJson } from "./fonts.js";
4
4
  import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
5
5
  import { View } from 'react-native';
6
6
  import { normalizeIndexScroll, normalizeKeyScroll, normalizePositionScroll, resolveLocationIndex, scrollFailure, validateOffset } from "./scrolling.js";
7
- import { applyRowPatches, containNativeListInput, emptyNativeListSnapshot, validatePatches, validateSnapshot } from "./validation.js";
7
+ import { applyRowPatches, containNativeListInput, emptyNativeListSnapshot, validateSnapshot } from "./validation.js";
8
8
  import { NativeListWebEngine } from "./web/NativeListWebEngine.js";
9
9
  import { acquireNativeListAvatar, canonicalNativeListAvatarUri } from "./web/NativeListWebAvatarCache.js";
10
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -255,18 +255,15 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
255
255
  synchronizeSlots(next);
256
256
  },
257
257
  applyPatches(patches) {
258
- const accepted = containNativeListInput(() => validatePatches(patches));
259
- if (accepted && accepted.length > 0) {
260
- const engine = engineRef.current;
261
- if (engine) {
262
- engine.applyPatches(accepted);
263
- snapshotRef.current = engine.currentSnapshot();
264
- } else {
265
- const next = containNativeListInput(() => applyRowPatches(snapshotRef.current, accepted));
266
- if (next) snapshotRef.current = next;
267
- }
268
- acceptedSnapshotRef.current = snapshotRef.current;
258
+ const engine = engineRef.current;
259
+ if (engine) {
260
+ engine.applyPatches(patches);
261
+ snapshotRef.current = engine.currentSnapshot();
262
+ } else {
263
+ const next = containNativeListInput(() => applyRowPatches(snapshotRef.current, patches));
264
+ if (next) snapshotRef.current = next;
269
265
  }
266
+ acceptedSnapshotRef.current = snapshotRef.current;
270
267
  },
271
268
  reconcileSelection(selectedKeys) {
272
269
  engineRef.current?.reconcileSelection(selectedKeys);
@@ -54,4 +54,6 @@ export const ROW_BOX_STYLE_KEYS_BY_TYPE = {
54
54
  // type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator';
55
55
  // OneKey patch: balance patches also refresh the existing row's spoken content.
56
56
  // type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator' | 'height' | 'heightRounding' | 'opacity' | 'pressDisabled';
57
+
58
+ /** Null removes optional fields; undefined leaves the existing value unchanged. */
57
59
  //# sourceMappingURL=models.js.map