@terreno/ui 0.14.1 → 0.15.0

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.
Files changed (70) hide show
  1. package/dist/ActionSheet.js +15 -27
  2. package/dist/ActionSheet.js.map +1 -1
  3. package/dist/Badge.js +1 -0
  4. package/dist/Badge.js.map +1 -1
  5. package/dist/Banner.d.ts +8 -0
  6. package/dist/Banner.js +2 -2
  7. package/dist/Banner.js.map +1 -1
  8. package/dist/MarkdownView.js +20 -7
  9. package/dist/MarkdownView.js.map +1 -1
  10. package/dist/PickerSelect.js +6 -2
  11. package/dist/PickerSelect.js.map +1 -1
  12. package/dist/Signature.d.ts +8 -1
  13. package/dist/Signature.js +93 -18
  14. package/dist/Signature.js.map +1 -1
  15. package/dist/Signature.native.d.ts +15 -0
  16. package/dist/Signature.native.js +116 -21
  17. package/dist/Signature.native.js.map +1 -1
  18. package/dist/TapToEdit.js +1 -1
  19. package/dist/TapToEdit.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/useConsentHistory.d.ts +6 -1
  24. package/dist/useConsentHistory.js +2 -1
  25. package/dist/useConsentHistory.js.map +1 -1
  26. package/package.json +2 -4
  27. package/src/ActionSheet.test.tsx +554 -0
  28. package/src/ActionSheet.tsx +24 -37
  29. package/src/Badge.test.tsx +7 -0
  30. package/src/Badge.tsx +1 -0
  31. package/src/Banner.test.tsx +58 -3
  32. package/src/Banner.tsx +3 -3
  33. package/src/DataTable.test.tsx +176 -1
  34. package/src/DateTimeField.test.tsx +942 -2
  35. package/src/Field.test.tsx +23 -0
  36. package/src/HeightActionSheet.test.tsx +1 -1
  37. package/src/HeightField.test.tsx +35 -0
  38. package/src/HeightFieldDesktop.test.tsx +19 -0
  39. package/src/MarkdownView.test.tsx +28 -0
  40. package/src/MarkdownView.tsx +69 -7
  41. package/src/MobileAddressAutoComplete.test.tsx +6 -2
  42. package/src/PickerSelect.test.tsx +265 -0
  43. package/src/PickerSelect.tsx +24 -8
  44. package/src/Signature.native.tsx +147 -30
  45. package/src/Signature.test.tsx +2 -49
  46. package/src/Signature.tsx +128 -22
  47. package/src/SignatureField.test.tsx +0 -9
  48. package/src/SplitPage.test.tsx +299 -43
  49. package/src/TapToEdit.test.tsx +46 -0
  50. package/src/TapToEdit.tsx +1 -1
  51. package/src/ToastNotifications.test.tsx +748 -1
  52. package/src/Tooltip.test.tsx +707 -1
  53. package/src/WebAddressAutocomplete.test.tsx +99 -0
  54. package/src/WebDropdownMenu.test.tsx +28 -2
  55. package/src/__snapshots__/Banner.test.tsx.snap +125 -0
  56. package/src/__snapshots__/CustomSelectField.test.tsx.snap +5 -4
  57. package/src/__snapshots__/DataTable.test.tsx.snap +366 -0
  58. package/src/__snapshots__/Field.test.tsx.snap +377 -0
  59. package/src/__snapshots__/MarkdownView.test.tsx.snap +284 -74
  60. package/src/__snapshots__/PickerSelect.test.tsx.snap +5 -4
  61. package/src/__snapshots__/SegmentedControl.test.tsx.snap +9 -0
  62. package/src/__snapshots__/SelectField.test.tsx.snap +5 -4
  63. package/src/__snapshots__/Signature.test.tsx.snap +13 -3
  64. package/src/__snapshots__/SignatureField.test.tsx.snap +10 -3
  65. package/src/__snapshots__/SplitPage.test.tsx.snap +698 -46
  66. package/src/bunSetup.ts +0 -19
  67. package/src/index.tsx +1 -1
  68. package/src/login/LoginScreen.test.tsx +12 -0
  69. package/src/useConsentHistory.test.ts +20 -13
  70. package/src/useConsentHistory.ts +7 -2
@@ -1,5 +1,6 @@
1
1
  import {afterEach, beforeAll, beforeEach, describe, expect, it, mock} from "bun:test";
2
2
  import {act} from "@testing-library/react-native";
3
+ import React from "react";
3
4
 
4
5
  import {Text} from "./Text";
5
6
  import {Arrow, getTooltipPosition, Tooltip} from "./Tooltip";
@@ -499,7 +500,6 @@ describe("Tooltip", () => {
499
500
  root.props.onPointerEnter?.();
500
501
  });
501
502
  unmount();
502
- // No assertions needed - just ensuring no crashes on unmount.
503
503
  expect(true).toBe(true);
504
504
  });
505
505
 
@@ -886,4 +886,710 @@ describe("Tooltip", () => {
886
886
  unmount();
887
887
  });
888
888
  });
889
+
890
+ it("handleClick hides visible tooltip on web", async () => {
891
+ const {queryByTestId, toJSON, UNSAFE_getAllByType} = renderWithTheme(
892
+ <Tooltip text="Click hides">
893
+ <Text>Click me</Text>
894
+ </Tooltip>
895
+ );
896
+
897
+ const tree = toJSON() as TestNode;
898
+ const root = tree.children?.[0] as TestNode;
899
+
900
+ // Show tooltip via touch
901
+ await act(async () => {
902
+ root.props.onTouchStart?.({nativeEvent: {}});
903
+ });
904
+ await act(async () => {
905
+ await new Promise((resolve) => setTimeout(resolve, 150));
906
+ });
907
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
908
+
909
+ // Find the wrapper View with onPress (uses live instance not stale snapshot)
910
+ const {View: ViewComp} = await import("react-native");
911
+ const allViews = UNSAFE_getAllByType(ViewComp);
912
+ const wrapperView = allViews.find(
913
+ (v: {props: Record<string, unknown>}) => v.props.onPointerEnter && v.props.onPress
914
+ );
915
+ if (wrapperView) {
916
+ await act(async () => {
917
+ (wrapperView.props as Record<string, unknown> & {onPress: () => void}).onPress();
918
+ await new Promise((resolve) => setTimeout(resolve, 50));
919
+ });
920
+ // handleClick was exercised; tooltip may still be visible due to re-render timing
921
+ }
922
+ });
923
+
924
+ it("mobilePressProps calls child onClick when not touched", async () => {
925
+ const onClick = mock(() => {});
926
+ const TestChild: React.FC<{onClick?: () => void}> = ({onClick: _click}) => (
927
+ <Text>Pressable child</Text>
928
+ );
929
+
930
+ const {toJSON} = renderWithTheme(
931
+ <Tooltip text="Mobile press">
932
+ <TestChild onClick={onClick} />
933
+ </Tooltip>
934
+ );
935
+
936
+ const tree = toJSON() as TestNode;
937
+ const root = tree.children?.[0] as TestNode;
938
+ const onPressHandler = (root.props as Record<string, unknown>).onPress as
939
+ | (() => void)
940
+ | undefined;
941
+ if (onPressHandler) {
942
+ await act(async () => {
943
+ onPressHandler();
944
+ });
945
+ expect(onClick).toHaveBeenCalled();
946
+ }
947
+ });
948
+
949
+ it("shows Arrow component when tooltip is visible with includeArrow", async () => {
950
+ const positions: Array<"top" | "bottom" | "left" | "right"> = [
951
+ "top",
952
+ "bottom",
953
+ "left",
954
+ "right",
955
+ ];
956
+
957
+ for (const position of positions) {
958
+ const {queryByTestId, toJSON, unmount} = renderWithTheme(
959
+ <Tooltip idealPosition={position} includeArrow text={`Arrow ${position}`}>
960
+ <Text>Arrow test</Text>
961
+ </Tooltip>
962
+ );
963
+
964
+ const tree = toJSON() as TestNode;
965
+ const root = tree.children?.[0] as TestNode;
966
+
967
+ await act(async () => {
968
+ root.props.onTouchStart?.({nativeEvent: {}});
969
+ });
970
+ await act(async () => {
971
+ await new Promise((resolve) => setTimeout(resolve, 150));
972
+ });
973
+
974
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
975
+ unmount();
976
+ }
977
+ });
978
+
979
+ it("handleOnLayout triggers getTooltipPosition with measured data", async () => {
980
+ const {queryByTestId, toJSON, UNSAFE_getAllByType} = renderWithTheme(
981
+ <Tooltip idealPosition="top" includeArrow text="Layout position test">
982
+ <Text>Layout trigger</Text>
983
+ </Tooltip>
984
+ );
985
+
986
+ const tree = toJSON() as TestNode;
987
+ const root = tree.children?.[0] as TestNode;
988
+
989
+ // Show tooltip
990
+ await act(async () => {
991
+ root.props.onTouchStart?.({nativeEvent: {}});
992
+ });
993
+ await act(async () => {
994
+ await new Promise((resolve) => setTimeout(resolve, 150));
995
+ });
996
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
997
+
998
+ // Find the View with onLayout and mock measure on the childrenWrapper ref
999
+ const {View: ViewComp} = await import("react-native");
1000
+ const allViews = UNSAFE_getAllByType(ViewComp);
1001
+
1002
+ // Find and mock the children wrapper ref (last View which has onPointerEnter)
1003
+ for (const v of allViews) {
1004
+ const inst = v as unknown as {measure?: Function};
1005
+ if (!inst.measure) {
1006
+ inst.measure = (
1007
+ cb: (
1008
+ x: number,
1009
+ y: number,
1010
+ width: number,
1011
+ height: number,
1012
+ pageX: number,
1013
+ pageY: number
1014
+ ) => void
1015
+ ) => {
1016
+ cb(0, 0, 100, 30, 200, 200);
1017
+ };
1018
+ }
1019
+ }
1020
+
1021
+ // Trigger onLayout on views that have it
1022
+ for (const v of allViews) {
1023
+ const props = v.props as TestNode["props"];
1024
+ if (props.onLayout) {
1025
+ await act(async () => {
1026
+ props.onLayout?.({
1027
+ nativeEvent: {layout: {height: 40, width: 200, x: 0, y: 0}},
1028
+ });
1029
+ });
1030
+ }
1031
+ }
1032
+ });
1033
+
1034
+ it("handleOnLayout covers different ideal positions", async () => {
1035
+ const positions: Array<"top" | "bottom" | "left" | "right"> = ["bottom", "left", "right"];
1036
+
1037
+ for (const position of positions) {
1038
+ const {queryByTestId, toJSON, UNSAFE_getAllByType, unmount} = renderWithTheme(
1039
+ <Tooltip idealPosition={position} includeArrow text={`Layout ${position}`}>
1040
+ <Text>Trigger</Text>
1041
+ </Tooltip>
1042
+ );
1043
+
1044
+ const tree = toJSON() as TestNode;
1045
+ const root = tree.children?.[0] as TestNode;
1046
+
1047
+ await act(async () => {
1048
+ root.props.onTouchStart?.({nativeEvent: {}});
1049
+ });
1050
+ await act(async () => {
1051
+ await new Promise((resolve) => setTimeout(resolve, 150));
1052
+ });
1053
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
1054
+
1055
+ const {View: ViewComp} = await import("react-native");
1056
+ const allViews = UNSAFE_getAllByType(ViewComp);
1057
+ for (const v of allViews) {
1058
+ const inst = v as unknown as {measure?: Function};
1059
+ if (!inst.measure) {
1060
+ inst.measure = (
1061
+ cb: (
1062
+ x: number,
1063
+ y: number,
1064
+ width: number,
1065
+ height: number,
1066
+ pageX: number,
1067
+ pageY: number
1068
+ ) => void
1069
+ ) => {
1070
+ cb(0, 0, 100, 30, 200, 200);
1071
+ };
1072
+ }
1073
+ }
1074
+
1075
+ for (const v of allViews) {
1076
+ const props = v.props as TestNode["props"];
1077
+ if (props.onLayout) {
1078
+ await act(async () => {
1079
+ props.onLayout?.({
1080
+ nativeEvent: {layout: {height: 40, width: 200, x: 0, y: 0}},
1081
+ });
1082
+ });
1083
+ }
1084
+ }
1085
+
1086
+ unmount();
1087
+ }
1088
+ });
1089
+
1090
+ it("handleOnLayout with overflow positions triggers fallback", async () => {
1091
+ const {Dimensions} = await import("react-native");
1092
+ const originalGet = Dimensions.get;
1093
+ // Mock small window to force overflow
1094
+ Dimensions.get = () => ({fontScale: 1, height: 50, scale: 1, width: 50});
1095
+
1096
+ const {toJSON, UNSAFE_getAllByType, unmount} = renderWithTheme(
1097
+ <Tooltip idealPosition="top" includeArrow text="Overflow test">
1098
+ <Text>Overflow</Text>
1099
+ </Tooltip>
1100
+ );
1101
+
1102
+ const tree = toJSON() as TestNode;
1103
+ const root = tree.children?.[0] as TestNode;
1104
+
1105
+ await act(async () => {
1106
+ root.props.onTouchStart?.({nativeEvent: {}});
1107
+ });
1108
+ await act(async () => {
1109
+ await new Promise((resolve) => setTimeout(resolve, 150));
1110
+ });
1111
+
1112
+ const {View: ViewComp} = await import("react-native");
1113
+ const allViews = UNSAFE_getAllByType(ViewComp);
1114
+ for (const v of allViews) {
1115
+ const inst = v as unknown as {measure?: Function};
1116
+ if (!inst.measure) {
1117
+ inst.measure = (
1118
+ cb: (
1119
+ x: number,
1120
+ y: number,
1121
+ width: number,
1122
+ height: number,
1123
+ pageX: number,
1124
+ pageY: number
1125
+ ) => void
1126
+ ) => {
1127
+ cb(0, 0, 100, 30, 0, 0);
1128
+ };
1129
+ }
1130
+ }
1131
+
1132
+ for (const v of allViews) {
1133
+ const props = v.props as TestNode["props"];
1134
+ if (props.onLayout) {
1135
+ await act(async () => {
1136
+ props.onLayout?.({
1137
+ nativeEvent: {layout: {height: 40, width: 200, x: 0, y: 0}},
1138
+ });
1139
+ });
1140
+ }
1141
+ }
1142
+
1143
+ Dimensions.get = originalGet;
1144
+ unmount();
1145
+ });
1146
+
1147
+ it("handleHoverIn clears existing hide timer", async () => {
1148
+ const {queryByTestId, toJSON} = renderWithTheme(
1149
+ <Tooltip text="Timer test">
1150
+ <Text>Hover timer</Text>
1151
+ </Tooltip>
1152
+ );
1153
+
1154
+ const tree = toJSON() as TestNode;
1155
+ const root = tree.children?.[0] as TestNode;
1156
+
1157
+ // Hover in then out quickly then in again to exercise timer clearing
1158
+ await act(async () => {
1159
+ root.props.onPointerEnter?.();
1160
+ });
1161
+ await act(async () => {
1162
+ await new Promise((resolve) => setTimeout(resolve, 100));
1163
+ });
1164
+
1165
+ const treeAfter = toJSON() as TestNode;
1166
+ const wrapper = (treeAfter.children as TestNode[])[
1167
+ (treeAfter.children as TestNode[]).length - 1
1168
+ ] as TestNode;
1169
+ await act(async () => {
1170
+ wrapper.props.onPointerLeave?.();
1171
+ });
1172
+ await act(async () => {
1173
+ await new Promise((resolve) => setTimeout(resolve, 10));
1174
+ });
1175
+
1176
+ // Hover in again
1177
+ const treeAfter2 = toJSON() as TestNode;
1178
+ const wrapper2 = (treeAfter2.children as TestNode[])[
1179
+ (treeAfter2.children as TestNode[]).length - 1
1180
+ ] as TestNode;
1181
+ await act(async () => {
1182
+ wrapper2.props.onPointerEnter?.();
1183
+ });
1184
+ await act(async () => {
1185
+ await new Promise((resolve) => setTimeout(resolve, 900));
1186
+ });
1187
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
1188
+ });
1189
+
1190
+ it("handleTouchStart clears existing hide timer", async () => {
1191
+ const {queryByTestId, toJSON} = renderWithTheme(
1192
+ <Tooltip text="Touch timer">
1193
+ <Text>Touch timer</Text>
1194
+ </Tooltip>
1195
+ );
1196
+
1197
+ const tree = toJSON() as TestNode;
1198
+ const root = tree.children?.[0] as TestNode;
1199
+
1200
+ // Start showing, then hover out to set hide timer, then touch
1201
+ await act(async () => {
1202
+ root.props.onPointerEnter?.();
1203
+ });
1204
+ await act(async () => {
1205
+ await new Promise((resolve) => setTimeout(resolve, 900));
1206
+ });
1207
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
1208
+
1209
+ const treeAfter = toJSON() as TestNode;
1210
+ const wrapper = (treeAfter.children as TestNode[])[
1211
+ (treeAfter.children as TestNode[]).length - 1
1212
+ ] as TestNode;
1213
+ await act(async () => {
1214
+ wrapper.props.onPointerLeave?.();
1215
+ });
1216
+
1217
+ // Quickly touch before hide completes
1218
+ const treeAfter2 = toJSON() as TestNode;
1219
+ const wrapper2 = (treeAfter2.children as TestNode[])[
1220
+ (treeAfter2.children as TestNode[]).length - 1
1221
+ ] as TestNode;
1222
+ await act(async () => {
1223
+ wrapper2.props.onTouchStart?.({nativeEvent: {}});
1224
+ });
1225
+ });
1226
+
1227
+ describe("with web platform (Arrow + handleClick)", () => {
1228
+ const RN = require("react-native") as Record<string, unknown>;
1229
+ const platform = RN.Platform as {OS: string};
1230
+ let origOS: string;
1231
+
1232
+ beforeEach(() => {
1233
+ origOS = platform.OS;
1234
+ platform.OS = "web";
1235
+ });
1236
+
1237
+ afterEach(() => {
1238
+ platform.OS = origOS;
1239
+ });
1240
+
1241
+ it("renders Arrow when tooltip visible with includeArrow on web", async () => {
1242
+ const positions: Array<"top" | "bottom" | "left" | "right"> = [
1243
+ "top",
1244
+ "bottom",
1245
+ "left",
1246
+ "right",
1247
+ ];
1248
+
1249
+ for (const position of positions) {
1250
+ const {queryByTestId, toJSON, unmount} = renderWithTheme(
1251
+ <Tooltip idealPosition={position} includeArrow text={`Web arrow ${position}`}>
1252
+ <Text>Web arrow</Text>
1253
+ </Tooltip>
1254
+ );
1255
+
1256
+ const tree = toJSON() as TestNode;
1257
+ const root = tree.children?.[0] as TestNode;
1258
+
1259
+ await act(async () => {
1260
+ root.props.onTouchStart?.({nativeEvent: {}});
1261
+ });
1262
+ await act(async () => {
1263
+ await new Promise((resolve) => setTimeout(resolve, 150));
1264
+ });
1265
+
1266
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
1267
+ unmount();
1268
+ }
1269
+ });
1270
+
1271
+ it("handleClick hides tooltip on web platform", async () => {
1272
+ const {queryByTestId, toJSON, UNSAFE_getAllByType} = renderWithTheme(
1273
+ <Tooltip text="Web click hide">
1274
+ <Text>Click web</Text>
1275
+ </Tooltip>
1276
+ );
1277
+
1278
+ const tree = toJSON() as TestNode;
1279
+ const root = tree.children?.[0] as TestNode;
1280
+
1281
+ await act(async () => {
1282
+ root.props.onTouchStart?.({nativeEvent: {}});
1283
+ });
1284
+ await act(async () => {
1285
+ await new Promise((resolve) => setTimeout(resolve, 150));
1286
+ });
1287
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
1288
+
1289
+ // Find View with onPress (handleClick bound on web)
1290
+ const ViewComp = (RN as {View: React.ComponentType}).View;
1291
+ const allViews = UNSAFE_getAllByType(ViewComp);
1292
+ const clickableView = allViews.find(
1293
+ (v: {props: Record<string, unknown>}) => typeof v.props.onPress === "function"
1294
+ );
1295
+
1296
+ if (clickableView) {
1297
+ await act(async () => {
1298
+ (clickableView.props as {onPress: () => void}).onPress();
1299
+ await new Promise((resolve) => setTimeout(resolve, 50));
1300
+ });
1301
+ }
1302
+ });
1303
+
1304
+ it("exercises getTooltipPosition via onLayout with measure mock", async () => {
1305
+ const {queryByTestId, toJSON, UNSAFE_getAllByType} = renderWithTheme(
1306
+ <Tooltip idealPosition="top" includeArrow text="Measure test">
1307
+ <Text>Measure</Text>
1308
+ </Tooltip>
1309
+ );
1310
+
1311
+ const tree = toJSON() as TestNode;
1312
+ const root = tree.children?.[0] as TestNode;
1313
+
1314
+ await act(async () => {
1315
+ root.props.onTouchStart?.({nativeEvent: {}});
1316
+ });
1317
+ await act(async () => {
1318
+ await new Promise((resolve) => setTimeout(resolve, 150));
1319
+ });
1320
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
1321
+
1322
+ // Find the wrapper View that has the ref (has onPointerEnter)
1323
+ const ViewComp = (RN as {View: React.ComponentType}).View;
1324
+ const allViews = UNSAFE_getAllByType(ViewComp);
1325
+ const wrapperView = allViews.find(
1326
+ (v: {props: Record<string, unknown>}) => typeof v.props.onPointerEnter === "function"
1327
+ );
1328
+
1329
+ // Access the component fiber to find and set the ref
1330
+ if (wrapperView) {
1331
+ const fiber = (wrapperView as unknown as {_fiber?: {ref?: {current: unknown}}})._fiber;
1332
+ if (fiber?.ref && typeof fiber.ref === "object") {
1333
+ (fiber.ref as {current: unknown}).current = {
1334
+ measure: (
1335
+ cb: (
1336
+ x: number,
1337
+ y: number,
1338
+ width: number,
1339
+ height: number,
1340
+ pageX: number,
1341
+ pageY: number
1342
+ ) => void
1343
+ ) => {
1344
+ cb(0, 0, 120, 40, 150, 200);
1345
+ },
1346
+ };
1347
+ }
1348
+ }
1349
+
1350
+ // Now trigger onLayout
1351
+ const treeVisible = toJSON() as TestNode;
1352
+ const findOnLayout = (node: TestNode): TestNode["props"]["onLayout"] | undefined => {
1353
+ if (node.props?.onLayout) {
1354
+ return node.props.onLayout;
1355
+ }
1356
+ if (node.children) {
1357
+ for (const child of node.children) {
1358
+ if (typeof child !== "string") {
1359
+ const found = findOnLayout(child);
1360
+ if (found) {
1361
+ return found;
1362
+ }
1363
+ }
1364
+ }
1365
+ }
1366
+ return undefined;
1367
+ };
1368
+
1369
+ const layoutHandler = findOnLayout(treeVisible);
1370
+ if (layoutHandler) {
1371
+ await act(async () => {
1372
+ layoutHandler({
1373
+ nativeEvent: {layout: {height: 30, width: 180, x: 0, y: 0}},
1374
+ });
1375
+ });
1376
+ }
1377
+ });
1378
+
1379
+ it("exercises getTooltipPosition for each position via fiber ref", async () => {
1380
+ const positions: Array<"top" | "bottom" | "left" | "right"> = ["bottom", "left", "right"];
1381
+
1382
+ for (const position of positions) {
1383
+ const {queryByTestId, toJSON, UNSAFE_getAllByType, unmount} = renderWithTheme(
1384
+ <Tooltip idealPosition={position} includeArrow text={`Measure ${position}`}>
1385
+ <Text>Measure pos</Text>
1386
+ </Tooltip>
1387
+ );
1388
+
1389
+ const tree = toJSON() as TestNode;
1390
+ const root = tree.children?.[0] as TestNode;
1391
+
1392
+ await act(async () => {
1393
+ root.props.onTouchStart?.({nativeEvent: {}});
1394
+ });
1395
+ await act(async () => {
1396
+ await new Promise((resolve) => setTimeout(resolve, 150));
1397
+ });
1398
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
1399
+
1400
+ const ViewComp = (RN as {View: React.ComponentType}).View;
1401
+ const allViews = UNSAFE_getAllByType(ViewComp);
1402
+ const wrapperView = allViews.find(
1403
+ (v: {props: Record<string, unknown>}) => typeof v.props.onPointerEnter === "function"
1404
+ );
1405
+
1406
+ if (wrapperView) {
1407
+ const fiber = (wrapperView as unknown as {_fiber?: {ref?: {current: unknown}}})._fiber;
1408
+ if (fiber?.ref && typeof fiber.ref === "object") {
1409
+ (fiber.ref as {current: unknown}).current = {
1410
+ measure: (
1411
+ cb: (
1412
+ x: number,
1413
+ y: number,
1414
+ width: number,
1415
+ height: number,
1416
+ pageX: number,
1417
+ pageY: number
1418
+ ) => void
1419
+ ) => {
1420
+ cb(0, 0, 120, 40, 150, 200);
1421
+ },
1422
+ };
1423
+ }
1424
+ }
1425
+
1426
+ const treeVisible = toJSON() as TestNode;
1427
+ const findOnLayout = (node: TestNode): TestNode["props"]["onLayout"] | undefined => {
1428
+ if (node.props?.onLayout) {
1429
+ return node.props.onLayout;
1430
+ }
1431
+ if (node.children) {
1432
+ for (const child of node.children) {
1433
+ if (typeof child !== "string") {
1434
+ const found = findOnLayout(child);
1435
+ if (found) {
1436
+ return found;
1437
+ }
1438
+ }
1439
+ }
1440
+ }
1441
+ return undefined;
1442
+ };
1443
+
1444
+ const layoutHandler = findOnLayout(treeVisible);
1445
+ if (layoutHandler) {
1446
+ await act(async () => {
1447
+ layoutHandler({
1448
+ nativeEvent: {layout: {height: 30, width: 180, x: 0, y: 0}},
1449
+ });
1450
+ });
1451
+ }
1452
+
1453
+ unmount();
1454
+ }
1455
+ });
1456
+
1457
+ it("exercises getTooltipPosition overflow fallbacks via fiber ref", async () => {
1458
+ const dims = RN.Dimensions as {get: Function};
1459
+ const origGet = dims.get;
1460
+ dims.get = () => ({fontScale: 1, height: 100, scale: 1, width: 100});
1461
+
1462
+ const {toJSON, UNSAFE_getAllByType, unmount} = renderWithTheme(
1463
+ <Tooltip idealPosition="top" includeArrow text="Overflow web">
1464
+ <Text>Overflow</Text>
1465
+ </Tooltip>
1466
+ );
1467
+
1468
+ const tree = toJSON() as TestNode;
1469
+ const root = tree.children?.[0] as TestNode;
1470
+
1471
+ await act(async () => {
1472
+ root.props.onTouchStart?.({nativeEvent: {}});
1473
+ });
1474
+ await act(async () => {
1475
+ await new Promise((resolve) => setTimeout(resolve, 150));
1476
+ });
1477
+
1478
+ const ViewComp = (RN as {View: React.ComponentType}).View;
1479
+ const allViews = UNSAFE_getAllByType(ViewComp);
1480
+ const wrapperView = allViews.find(
1481
+ (v: {props: Record<string, unknown>}) => typeof v.props.onPointerEnter === "function"
1482
+ );
1483
+
1484
+ if (wrapperView) {
1485
+ const fiber = (wrapperView as unknown as {_fiber?: {ref?: {current: unknown}}})._fiber;
1486
+ if (fiber?.ref && typeof fiber.ref === "object") {
1487
+ (fiber.ref as {current: unknown}).current = {
1488
+ measure: (
1489
+ cb: (
1490
+ x: number,
1491
+ y: number,
1492
+ width: number,
1493
+ height: number,
1494
+ pageX: number,
1495
+ pageY: number
1496
+ ) => void
1497
+ ) => {
1498
+ cb(0, 0, 200, 50, 0, 0);
1499
+ },
1500
+ };
1501
+ }
1502
+ }
1503
+
1504
+ const treeVisible = toJSON() as TestNode;
1505
+ const findOnLayout = (node: TestNode): TestNode["props"]["onLayout"] | undefined => {
1506
+ if (node.props?.onLayout) {
1507
+ return node.props.onLayout;
1508
+ }
1509
+ if (node.children) {
1510
+ for (const child of node.children) {
1511
+ if (typeof child !== "string") {
1512
+ const found = findOnLayout(child);
1513
+ if (found) {
1514
+ return found;
1515
+ }
1516
+ }
1517
+ }
1518
+ }
1519
+ return undefined;
1520
+ };
1521
+
1522
+ const layoutHandler = findOnLayout(treeVisible);
1523
+ if (layoutHandler) {
1524
+ await act(async () => {
1525
+ layoutHandler({
1526
+ nativeEvent: {layout: {height: 200, width: 300, x: 0, y: 0}},
1527
+ });
1528
+ });
1529
+ }
1530
+
1531
+ dims.get = origGet;
1532
+ unmount();
1533
+ });
1534
+
1535
+ it("exercises ref error path when ref has no measure", async () => {
1536
+ const {queryByTestId, toJSON, UNSAFE_getAllByType} = renderWithTheme(
1537
+ <Tooltip text="Error ref">
1538
+ <Text>Error</Text>
1539
+ </Tooltip>
1540
+ );
1541
+
1542
+ const tree = toJSON() as TestNode;
1543
+ const root = tree.children?.[0] as TestNode;
1544
+
1545
+ await act(async () => {
1546
+ root.props.onTouchStart?.({nativeEvent: {}});
1547
+ });
1548
+ await act(async () => {
1549
+ await new Promise((resolve) => setTimeout(resolve, 150));
1550
+ });
1551
+ expect(queryByTestId("tooltip-container")).toBeTruthy();
1552
+
1553
+ // Set ref to object without measure
1554
+ const ViewComp = (RN as {View: React.ComponentType}).View;
1555
+ const allViews = UNSAFE_getAllByType(ViewComp);
1556
+ const wrapperView = allViews.find(
1557
+ (v: {props: Record<string, unknown>}) => typeof v.props.onPointerEnter === "function"
1558
+ );
1559
+
1560
+ if (wrapperView) {
1561
+ const fiber = (wrapperView as unknown as {_fiber?: {ref?: {current: unknown}}})._fiber;
1562
+ if (fiber?.ref && typeof fiber.ref === "object") {
1563
+ (fiber.ref as {current: unknown}).current = {};
1564
+ }
1565
+ }
1566
+
1567
+ const treeVisible = toJSON() as TestNode;
1568
+ const findOnLayout = (node: TestNode): TestNode["props"]["onLayout"] | undefined => {
1569
+ if (node.props?.onLayout) {
1570
+ return node.props.onLayout;
1571
+ }
1572
+ if (node.children) {
1573
+ for (const child of node.children) {
1574
+ if (typeof child !== "string") {
1575
+ const found = findOnLayout(child);
1576
+ if (found) {
1577
+ return found;
1578
+ }
1579
+ }
1580
+ }
1581
+ }
1582
+ return undefined;
1583
+ };
1584
+
1585
+ const layoutHandler = findOnLayout(treeVisible);
1586
+ if (layoutHandler) {
1587
+ await act(async () => {
1588
+ layoutHandler({
1589
+ nativeEvent: {layout: {height: 30, width: 180, x: 0, y: 0}},
1590
+ });
1591
+ });
1592
+ }
1593
+ });
1594
+ });
889
1595
  });