@momo-kits/foundation 0.169.7-beta.2 → 0.169.7-beta.4
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.
- package/Application/BottomSheet.hooks.ts +8 -8
- package/Application/BottomSheet.parts.tsx +34 -44
- package/Application/BottomSheet.tsx +37 -40
- package/Application/BottomTab/ActiveIndicator.tsx +14 -15
- package/Application/BottomTab/BottomTabBar.tsx +62 -49
- package/Application/BottomTab/BottomTabInset.tsx +4 -8
- package/Application/BottomTab/CurvedContainer.tsx +12 -14
- package/Application/BottomTab/CustomBottomTabItem.tsx +13 -13
- package/Application/BottomTab/DuoBarCompression.tsx +19 -13
- package/Application/BottomTab/DuoRailTabBar.tsx +24 -20
- package/Application/BottomTab/DuoTabBar.tsx +10 -8
- package/Application/BottomTab/TabBarIcon.tsx +29 -15
- package/Application/BottomTab/TabScreen.tsx +5 -1
- package/Application/BottomTab/index.tsx +30 -33
- package/Application/BottomTab/metrics.ts +23 -23
- package/Application/BottomTab/tabItems.ts +1 -1
- package/Application/BottomTab/usePressLift.ts +2 -2
- package/Application/Components/BackgroundImageView.tsx +0 -4
- package/Application/Components/DuoGlassBackground.tsx +4 -5
- package/Application/Components/HeaderAnimated.tsx +0 -4
- package/Application/Components/HeaderBackground.tsx +0 -4
- package/Application/Components/HeaderExtendHeader.tsx +10 -11
- package/Application/Components/HeaderLeft.tsx +1 -2
- package/Application/Components/HeaderRight.tsx +1 -2
- package/Application/Components/HeaderTitle.shared.ts +9 -11
- package/Application/Components/HeaderTitle.tsx +3 -4
- package/Application/Components/HeaderToolkitAction.tsx +20 -17
- package/Application/Components/NavigationButton.tsx +1 -2
- package/Application/Components/SearchHeader.tsx +10 -17
- package/Application/Components/TitleJourney.tsx +2 -2
- package/Application/Components/TitleLocation.tsx +2 -2
- package/Application/Components/TitleUser.tsx +2 -2
- package/Application/Components/useToolkitActions.tsx +32 -17
- package/Application/KitAdoption/AdoptionBadge.tsx +119 -0
- package/Application/KitAdoption/AdoptionProbe.tsx +166 -0
- package/Application/KitAdoption/AdoptionStore.ts +80 -0
- package/Application/KitAdoption/Authorship/utils.ts +137 -0
- package/Application/KitAdoption/Slots/utils.ts +68 -0
- package/Application/KitAdoption/constants.ts +120 -0
- package/Application/KitAdoption/index.ts +12 -0
- package/Application/KitAdoption/styles.ts +55 -0
- package/Application/KitAdoption/types.ts +61 -0
- package/Application/KitAdoption/utils.ts +376 -0
- package/Application/Localize.ts +0 -7
- package/Application/ModalScreen.tsx +10 -10
- package/Application/Navigation.ts +16 -14
- package/Application/NavigationContainer.tsx +18 -19
- package/Application/Navigator.ts +48 -20
- package/Application/ScaleSizeProvider.tsx +6 -13
- package/Application/StackScreen.tracking.ts +33 -11
- package/Application/StackScreen.tsx +54 -10
- package/Application/TooltipPortal.tsx +5 -12
- package/Application/WidgetContainer.tsx +4 -10
- package/Application/__tests__/KitAdoption.integration.test.tsx +253 -0
- package/Application/__tests__/KitAdoption.test.ts +283 -0
- package/Application/__tests__/KitAdoptionAuthorship.test.ts +132 -0
- package/Application/__tests__/KitAdoptionStore.test.ts +110 -0
- package/Application/__tests__/KitAdoptionStructural.test.ts +193 -0
- package/Application/__tests__/modalLayout.test.ts +128 -0
- package/Application/modalLayout.ts +14 -16
- package/Application/navigationOptions.tsx +17 -21
- package/Badge/Badge.tsx +0 -5
- package/Badge/BadgeDot.tsx +0 -1
- package/Badge/BadgeDotAnimation.tsx +7 -15
- package/Badge/BadgeRibbon.tsx +38 -46
- package/Badge/Shape.tsx +0 -2
- package/Badge/types.ts +2 -2
- package/Button/index.tsx +16 -5
- package/Button/utils.ts +15 -9
- package/CheckBox/index.tsx +0 -4
- package/Consts/theme.ts +10 -10
- package/Context/index.ts +1 -5
- package/Divider/DashDivider.tsx +0 -1
- package/Divider/index.tsx +0 -4
- package/FoundationList/index.tsx +0 -4
- package/Icon/index.tsx +6 -6
- package/Icon/types.ts +1 -1
- package/IconButton/index.tsx +12 -4
- package/Image/index.tsx +7 -9
- package/Input/Input.tsx +9 -8
- package/Input/InputDropDown.tsx +12 -10
- package/Input/InputMoney.tsx +8 -12
- package/Input/InputMoneyView.tsx +7 -14
- package/Input/InputOTP.tsx +33 -35
- package/Input/InputPhoneNumber.tsx +9 -8
- package/Input/InputSearch.tsx +6 -4
- package/Input/InputTextArea.tsx +0 -4
- package/Input/SearchTextTyping.tsx +0 -4
- package/Input/SystemTextInput.tsx +2 -7
- package/Input/TextTyping.tsx +0 -4
- package/Input/common.tsx +12 -26
- package/Input/index.tsx +20 -17
- package/Input/moneyUtils.ts +4 -10
- package/Input/styles.ts +5 -0
- package/Input/utils.ts +12 -14
- package/Layout/Card.tsx +4 -7
- package/Layout/FloatingButton.tsx +0 -4
- package/Layout/Item.tsx +2 -6
- package/Layout/ItemList.tsx +7 -9
- package/Layout/ItemSectionList.tsx +1 -2
- package/Layout/ReservedRegions.tsx +25 -25
- package/Layout/Screen.tsx +58 -56
- package/Layout/Section.tsx +5 -8
- package/Layout/TrackingScope.tsx +0 -1
- package/Layout/screenContent.tsx +2 -4
- package/Layout/screenFooter.tsx +18 -28
- package/Layout/screenHeader.tsx +31 -34
- package/Layout/screenQuickReturn.tsx +20 -17
- package/Layout/screenScroll.ts +7 -4
- package/Layout/screenTypes.ts +2 -3
- package/Layout/types.ts +4 -2
- package/Layout/utils.ts +37 -31
- package/Loader/DotLoader.tsx +0 -4
- package/Loader/Loader.tsx +0 -4
- package/Loader/ProgressBar.tsx +0 -1
- package/Loader/Spinner.tsx +0 -4
- package/Loader/utils.ts +7 -11
- package/Pagination/Dot.tsx +0 -4
- package/Pagination/PaginationDot.tsx +0 -1
- package/Pagination/PaginationNumber.tsx +3 -5
- package/Pagination/PaginationScroll.tsx +0 -5
- package/Pagination/PaginationWhiteDot.tsx +0 -1
- package/Pagination/index.tsx +0 -4
- package/Pagination/types.ts +2 -2
- package/Popup/PopupNotify.tsx +48 -44
- package/Popup/PopupPromotion.tsx +7 -4
- package/Popup/types.ts +4 -4
- package/Popup/utils.ts +0 -4
- package/Radio/index.tsx +0 -4
- package/Skeleton/index.tsx +0 -8
- package/Switch/index.tsx +0 -1
- package/Tag/index.tsx +1 -5
- package/Tag/types.ts +3 -3
- package/Text/index.tsx +3 -14
- package/Text/styles.ts +1 -1
- package/Text/types.ts +6 -1
- package/Text/utils.ts +4 -20
- package/Title/index.tsx +32 -41
- package/ViewBoundary/index.tsx +2 -4
- package/index.ts +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
jest.unmock('react-native');
|
|
2
|
+
|
|
3
|
+
/* Native packages are stubbed: the real modules are ESM or need native code under jest. */
|
|
4
|
+
jest.mock('react-native-reanimated', () => {
|
|
5
|
+
const { View, Text, Image } = require('react-native');
|
|
6
|
+
const Animated = {
|
|
7
|
+
View,
|
|
8
|
+
Text,
|
|
9
|
+
Image,
|
|
10
|
+
createAnimatedComponent: (component: unknown) => component,
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: Animated,
|
|
15
|
+
...Animated,
|
|
16
|
+
useSharedValue: (value: unknown) => ({ value }),
|
|
17
|
+
useAnimatedStyle: () => ({}),
|
|
18
|
+
useDerivedValue: (fn: () => unknown) => ({ value: fn() }),
|
|
19
|
+
withTiming: (value: unknown) => value,
|
|
20
|
+
withSpring: (value: unknown) => value,
|
|
21
|
+
runOnJS: (fn: unknown) => fn,
|
|
22
|
+
interpolate: () => 0,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
jest.mock('react-native-fast-image', () => {
|
|
26
|
+
const mockReact = require('react');
|
|
27
|
+
const FastImageStub = (props: Record<string, unknown>) =>
|
|
28
|
+
mockReact.createElement('FastImage', props);
|
|
29
|
+
return { __esModule: true, default: FastImageStub };
|
|
30
|
+
});
|
|
31
|
+
jest.mock('react-native-linear-gradient', () => {
|
|
32
|
+
const mockReact = require('react');
|
|
33
|
+
const LinearGradientStub = (props: Record<string, unknown>) =>
|
|
34
|
+
mockReact.createElement('LinearGradient', props);
|
|
35
|
+
return { __esModule: true, default: LinearGradientStub };
|
|
36
|
+
});
|
|
37
|
+
jest.mock('lottie-react-native', () => {
|
|
38
|
+
const mockReact = require('react');
|
|
39
|
+
const LottieStub = () => null;
|
|
40
|
+
return { __esModule: true, default: mockReact.forwardRef(LottieStub) };
|
|
41
|
+
});
|
|
42
|
+
jest.mock('../../Application', () => ({
|
|
43
|
+
useComponentId: () => ({ componentId: 'test-id' }),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
import React, { Fragment, type ElementType, type ReactNode } from 'react';
|
|
47
|
+
import {
|
|
48
|
+
FlatList,
|
|
49
|
+
Image,
|
|
50
|
+
Text as RNText,
|
|
51
|
+
TouchableOpacity,
|
|
52
|
+
View,
|
|
53
|
+
} from 'react-native';
|
|
54
|
+
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
|
55
|
+
|
|
56
|
+
import { Button as FoundationButton } from '../../Button';
|
|
57
|
+
import { Text as FoundationText } from '../../Text';
|
|
58
|
+
import {
|
|
59
|
+
createRegistries,
|
|
60
|
+
getCurrentFiber,
|
|
61
|
+
walkScreen,
|
|
62
|
+
} from '../KitAdoption/utils';
|
|
63
|
+
import type { FiberLike, WalkResult } from '../KitAdoption/types';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Production jsx of the React build. Elements made with it carry no
|
|
67
|
+
* `_debugStack`, the same as mini-app code shipped as a prod bundle. Loaded by
|
|
68
|
+
* resolved path: the react `exports` map blocks cjs/ under jest.
|
|
69
|
+
*/
|
|
70
|
+
const reactEntry = (
|
|
71
|
+
require as unknown as { resolve: (id: string) => string }
|
|
72
|
+
).resolve('react');
|
|
73
|
+
const prodJsxRuntime = require(reactEntry.replace(
|
|
74
|
+
/index\.js$/,
|
|
75
|
+
'cjs/react-jsx-runtime.production.js',
|
|
76
|
+
)) as {
|
|
77
|
+
jsx: (
|
|
78
|
+
type: ElementType,
|
|
79
|
+
props: Record<string, unknown>,
|
|
80
|
+
) => React.ReactElement;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Builds an element with the prod jsx runtime, the way a mini-app prod bundle does. */
|
|
84
|
+
const prodJsx = (
|
|
85
|
+
type: ElementType,
|
|
86
|
+
props: Record<string, unknown>,
|
|
87
|
+
...children: ReactNode[]
|
|
88
|
+
): React.ReactElement => {
|
|
89
|
+
if (children.length === 0) return prodJsxRuntime.jsx(type, props);
|
|
90
|
+
return prodJsxRuntime.jsx(type, {
|
|
91
|
+
...props,
|
|
92
|
+
children: children.length > 1 ? children : children[0],
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Test-only probe: exposes its fiber so the measurement starts from the real tree. */
|
|
97
|
+
class Probe extends React.Component<{ children?: ReactNode }> {
|
|
98
|
+
render(): ReactNode {
|
|
99
|
+
return this.props.children;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Kit registry built from the components this test renders. The full foundation
|
|
105
|
+
* index cannot load under jest (navigation and native-only packages), so it is
|
|
106
|
+
* not used. TouchableOpacity is listed as the foundation re-export, which must not count as kit.
|
|
107
|
+
*/
|
|
108
|
+
const registries = createRegistries(
|
|
109
|
+
[{ Button: FoundationButton, Text: FoundationText }, { TouchableOpacity }],
|
|
110
|
+
require('react-native') as Record<string, unknown>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/** Renders the tree under a Probe and walks the probe's child, as AdoptionProbe does. */
|
|
114
|
+
const measure = (tree: ReactNode, isStructural = false): WalkResult => {
|
|
115
|
+
let instance: Probe | null = null;
|
|
116
|
+
let renderer: ReactTestRenderer | undefined;
|
|
117
|
+
act(() => {
|
|
118
|
+
renderer = create(
|
|
119
|
+
<Probe
|
|
120
|
+
ref={(value: Probe | null) => {
|
|
121
|
+
instance = value;
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{tree}
|
|
125
|
+
</Probe>,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
const fiber = (instance as unknown as { _reactInternals: FiberLike })
|
|
129
|
+
._reactInternals;
|
|
130
|
+
const root = getCurrentFiber(fiber).child;
|
|
131
|
+
if (!root) throw new Error('probe rendered no child fiber');
|
|
132
|
+
const result = walkScreen(
|
|
133
|
+
root,
|
|
134
|
+
registries,
|
|
135
|
+
undefined,
|
|
136
|
+
undefined,
|
|
137
|
+
isStructural,
|
|
138
|
+
);
|
|
139
|
+
act(() => {
|
|
140
|
+
renderer?.unmount();
|
|
141
|
+
});
|
|
142
|
+
return result;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Shorthand for the K/R/O triple the demo screen prints. */
|
|
146
|
+
const kro = (result: WalkResult): string =>
|
|
147
|
+
`K${result.counts.K} R${result.counts.R} O${result.counts.O}`;
|
|
148
|
+
|
|
149
|
+
describe('KitAdoption integration (prod-built app tree over dev host)', () => {
|
|
150
|
+
it('a. kit-only tree: kit components counted, their internals not', () => {
|
|
151
|
+
const result = measure(
|
|
152
|
+
prodJsx(
|
|
153
|
+
View,
|
|
154
|
+
{},
|
|
155
|
+
prodJsx(FoundationText, {}, 'a'),
|
|
156
|
+
prodJsx(FoundationButton, { title: 'b' }),
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(kro(result)).toBe('K2 R1 O0');
|
|
161
|
+
expect(result.status).toBe('ok');
|
|
162
|
+
expect(result.kit).toEqual({ Text: 1, Button: 1 });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('b. app wrapper is transparent, each use of the kit inside it is counted', () => {
|
|
166
|
+
// Two buttons are siblings, so they sit under one dev root, like a real screen.
|
|
167
|
+
// The probe walks a single root and not its siblings (see walkScreen).
|
|
168
|
+
function MyButton(props: { title: string }): React.ReactElement {
|
|
169
|
+
return prodJsx(FoundationButton, props);
|
|
170
|
+
}
|
|
171
|
+
function Screen(): React.ReactElement {
|
|
172
|
+
return prodJsx(
|
|
173
|
+
Fragment,
|
|
174
|
+
{},
|
|
175
|
+
prodJsx(MyButton, { title: '1' }),
|
|
176
|
+
prodJsx(MyButton, { title: '2' }),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const result = measure(<Screen />);
|
|
180
|
+
|
|
181
|
+
expect(result.counts.K).toBe(2);
|
|
182
|
+
expect(kro(result)).toBe('K2 R0 O0');
|
|
183
|
+
expect(result.top_transparent).toEqual([{ name: 'MyButton', count: 2 }]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('c. raw react-native primitives written by the app count as R', () => {
|
|
187
|
+
const result = measure(
|
|
188
|
+
prodJsx(
|
|
189
|
+
View,
|
|
190
|
+
{},
|
|
191
|
+
prodJsx(RNText, {}, 'x'),
|
|
192
|
+
prodJsx(Image, { source: { uri: 'https://example.com/a.png' } }),
|
|
193
|
+
prodJsx(TouchableOpacity, {}),
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(kro(result)).toBe('K0 R4 O0');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('d. app child in a kit container counts K, the container internals do not', () => {
|
|
201
|
+
const result = measure(
|
|
202
|
+
prodJsx(FoundationText, {}, prodJsx(FoundationText, {}, 'child')),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(kro(result)).toBe('K2 R0 O0');
|
|
206
|
+
expect(result.kit).toEqual({ Text: 2 });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('e. FlatList counts R once, each renderItem element counts K', () => {
|
|
210
|
+
const items = ['one', 'two', 'three'];
|
|
211
|
+
const result = measure(
|
|
212
|
+
prodJsx(FlatList, {
|
|
213
|
+
data: items,
|
|
214
|
+
keyExtractor: (item: string) => item,
|
|
215
|
+
renderItem: ({ item }: { item: string }) =>
|
|
216
|
+
prodJsx(FoundationText, {}, item),
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect(kro(result)).toBe('K3 R1 O0');
|
|
221
|
+
expect(result.rn).toEqual({ FlatList: 1 });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('f. foundation re-export of TouchableOpacity counts as R, not K', () => {
|
|
225
|
+
const result = measure(prodJsx(TouchableOpacity, {}, 'x'));
|
|
226
|
+
|
|
227
|
+
expect(kro(result)).toBe('K0 R1 O0');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('g. an all-dev tree is reported as dev_bundle', () => {
|
|
231
|
+
const result = measure(
|
|
232
|
+
<View>
|
|
233
|
+
<RNText>x</RNText>
|
|
234
|
+
</View>,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(result.status).toBe('dev_bundle');
|
|
238
|
+
expect(result.counts).toEqual({ K: 0, R: 0, O: 0 });
|
|
239
|
+
expect(result.fibers).toBeGreaterThan(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('h. structural mode matches dev mode on the FlatList scenario', () => {
|
|
243
|
+
const tree = (): React.ReactElement =>
|
|
244
|
+
prodJsx(FlatList, {
|
|
245
|
+
data: ['one', 'two', 'three'],
|
|
246
|
+
keyExtractor: (item: string) => item,
|
|
247
|
+
renderItem: ({ item }: { item: string }) =>
|
|
248
|
+
prodJsx(FoundationText, {}, item),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(kro(measure(tree(), true))).toBe(kro(measure(tree())));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { createElement, Fragment } from 'react';
|
|
2
|
+
|
|
3
|
+
import { FIBER_TAG, REACT_MEMO_TYPE } from '../KitAdoption/constants';
|
|
4
|
+
import type { FiberLike } from '../KitAdoption/types';
|
|
5
|
+
import {
|
|
6
|
+
createRegistries,
|
|
7
|
+
getCurrentFiber,
|
|
8
|
+
walkScreen,
|
|
9
|
+
} from '../KitAdoption/utils';
|
|
10
|
+
|
|
11
|
+
const STACK = 'dev-stack';
|
|
12
|
+
|
|
13
|
+
const Button = () => null;
|
|
14
|
+
const Card = () => null;
|
|
15
|
+
const Chip = () => null;
|
|
16
|
+
const MyButton = () => null;
|
|
17
|
+
const Plain = () => null;
|
|
18
|
+
const RNView = () => null;
|
|
19
|
+
const Text = () => null;
|
|
20
|
+
const TouchableOpacity = () => null;
|
|
21
|
+
|
|
22
|
+
const makeFiber = (overrides: Partial<FiberLike> = {}): FiberLike => ({
|
|
23
|
+
tag: FIBER_TAG.FunctionComponent,
|
|
24
|
+
type: undefined,
|
|
25
|
+
child: null,
|
|
26
|
+
sibling: null,
|
|
27
|
+
return: null,
|
|
28
|
+
alternate: null,
|
|
29
|
+
memoizedProps: {},
|
|
30
|
+
...overrides,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** Sets children of parent (child, sibling, return links) and returns parent. */
|
|
34
|
+
const link = (parent: FiberLike, children: FiberLike[]): FiberLike => {
|
|
35
|
+
children.forEach((child, index) => {
|
|
36
|
+
child.return = parent;
|
|
37
|
+
child.sibling = children[index + 1] ?? null;
|
|
38
|
+
});
|
|
39
|
+
parent.child = children[0] ?? null;
|
|
40
|
+
return parent;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Linear chain of plain app fibers, returned from the top. */
|
|
44
|
+
const buildChain = (length: number): FiberLike => {
|
|
45
|
+
let head = makeFiber({ type: Plain });
|
|
46
|
+
for (let i = 1; i < length; i += 1) {
|
|
47
|
+
head = link(makeFiber({ type: Plain }), [head]);
|
|
48
|
+
}
|
|
49
|
+
return head;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
describe('KitAdoption walkScreen', () => {
|
|
53
|
+
it('counts a kit component once and skips its host-internal fibers', () => {
|
|
54
|
+
const internalView = makeFiber({
|
|
55
|
+
tag: FIBER_TAG.HostComponent,
|
|
56
|
+
type: 'RCTView',
|
|
57
|
+
_debugStack: STACK,
|
|
58
|
+
});
|
|
59
|
+
const button = link(makeFiber({ type: Button }), [internalView]);
|
|
60
|
+
const myButton = link(makeFiber({ type: MyButton }), [button]);
|
|
61
|
+
|
|
62
|
+
const result = walkScreen(
|
|
63
|
+
myButton,
|
|
64
|
+
createRegistries([{ Button }], { View: RNView }),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(result.counts.K).toBe(1);
|
|
68
|
+
expect(result.counts.R).toBe(0);
|
|
69
|
+
expect(result.kit.Button).toBe(1);
|
|
70
|
+
expect(result.top_transparent).toEqual([{ name: 'MyButton', count: 1 }]);
|
|
71
|
+
expect(result.status).toBe('ok');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('counts a raw RN View written by the app as R', () => {
|
|
75
|
+
const result = walkScreen(
|
|
76
|
+
makeFiber({ type: RNView }),
|
|
77
|
+
createRegistries([{ Button }], { View: RNView }),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(result.counts.R).toBe(1);
|
|
81
|
+
expect(result.counts.K).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('counts an app Text inside a kit Card as K and ignores the Card host view', () => {
|
|
85
|
+
const cardInternalView = makeFiber({
|
|
86
|
+
tag: FIBER_TAG.HostComponent,
|
|
87
|
+
type: 'RCTView',
|
|
88
|
+
_debugStack: STACK,
|
|
89
|
+
});
|
|
90
|
+
const appText = makeFiber({ type: Text });
|
|
91
|
+
const card = link(makeFiber({ type: Card }), [cardInternalView, appText]);
|
|
92
|
+
|
|
93
|
+
const result = walkScreen(
|
|
94
|
+
card,
|
|
95
|
+
createRegistries([{ Card, Text: Text }], { Text: Text }),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result.counts.K).toBe(2);
|
|
99
|
+
expect(result.counts.R).toBe(0);
|
|
100
|
+
expect(result.kit.Card).toBe(1);
|
|
101
|
+
expect(result.kit.Text).toBe(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('counts a lib unit once and only the slot children the app passed in', () => {
|
|
105
|
+
const slotElement = createElement(Button, { label: 'slot' });
|
|
106
|
+
const internalButton = makeFiber({ type: Button, memoizedProps: {} });
|
|
107
|
+
const slotFiber = makeFiber({
|
|
108
|
+
type: Button,
|
|
109
|
+
memoizedProps: slotElement.props,
|
|
110
|
+
});
|
|
111
|
+
const chip = link(
|
|
112
|
+
makeFiber({ type: Chip, memoizedProps: { children: slotElement } }),
|
|
113
|
+
[internalButton, slotFiber],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const result = walkScreen(chip, createRegistries([{ Button }], {}));
|
|
117
|
+
|
|
118
|
+
expect(result.counts.K).toBe(2);
|
|
119
|
+
expect(result.kit.Chip).toBe(1);
|
|
120
|
+
expect(result.kit.Button).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('treats the children of a Fragment passed into a lib unit as slots', () => {
|
|
124
|
+
const first = createElement(Button, { label: 'first' });
|
|
125
|
+
const second = createElement(Button, { label: 'second' });
|
|
126
|
+
const fragment = createElement(Fragment, null, first, second);
|
|
127
|
+
const chip = link(
|
|
128
|
+
makeFiber({ type: Chip, memoizedProps: { children: fragment } }),
|
|
129
|
+
[
|
|
130
|
+
makeFiber({ type: Button, memoizedProps: {} }),
|
|
131
|
+
makeFiber({ type: Button, memoizedProps: first.props }),
|
|
132
|
+
makeFiber({ type: Button, memoizedProps: second.props }),
|
|
133
|
+
],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const result = walkScreen(chip, createRegistries([{ Button }], {}));
|
|
137
|
+
|
|
138
|
+
expect(result.kit.Button).toBe(2);
|
|
139
|
+
expect(result.counts.K).toBe(3);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('does not count the TouchableOpacity re-export as kit', () => {
|
|
143
|
+
const registries = createRegistries([{ TouchableOpacity }], {
|
|
144
|
+
TouchableOpacity,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const result = walkScreen(
|
|
148
|
+
makeFiber({ type: TouchableOpacity }),
|
|
149
|
+
registries,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(result.counts.K).toBe(0);
|
|
153
|
+
expect(result.counts.R).toBe(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('reports truncated when the walk exceeds the fiber limit', () => {
|
|
157
|
+
const result = walkScreen(
|
|
158
|
+
buildChain(10),
|
|
159
|
+
createRegistries([], {}),
|
|
160
|
+
undefined,
|
|
161
|
+
5,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(result.truncated).toBe(true);
|
|
165
|
+
expect(result.status).toBe('truncated');
|
|
166
|
+
expect(result.fibers).toBe(5);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('reports dev_bundle when every fiber carries a dev stack', () => {
|
|
170
|
+
const child = makeFiber({
|
|
171
|
+
tag: FIBER_TAG.HostComponent,
|
|
172
|
+
type: 'RCTView',
|
|
173
|
+
_debugStack: STACK,
|
|
174
|
+
});
|
|
175
|
+
const root = link(
|
|
176
|
+
makeFiber({
|
|
177
|
+
tag: FIBER_TAG.HostComponent,
|
|
178
|
+
type: 'RCTView',
|
|
179
|
+
_debugStack: STACK,
|
|
180
|
+
}),
|
|
181
|
+
[child],
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const result = walkScreen(root, createRegistries([{ Button }], {}));
|
|
185
|
+
|
|
186
|
+
expect(result.status).toBe('dev_bundle');
|
|
187
|
+
expect(result.counts).toEqual({ K: 0, R: 0, O: 0 });
|
|
188
|
+
expect(result.fibers).toBe(2);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('does not count the inner fiber of a host-created memo as app code', () => {
|
|
192
|
+
const innerView = makeFiber({ type: RNView });
|
|
193
|
+
const hostMemo = link(
|
|
194
|
+
makeFiber({
|
|
195
|
+
tag: FIBER_TAG.MemoComponent,
|
|
196
|
+
type: { $$typeof: REACT_MEMO_TYPE, type: RNView },
|
|
197
|
+
_debugStack: STACK,
|
|
198
|
+
}),
|
|
199
|
+
[innerView],
|
|
200
|
+
);
|
|
201
|
+
const hostKit = link(makeFiber({ type: Button, _debugStack: STACK }), [
|
|
202
|
+
hostMemo,
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
const result = walkScreen(
|
|
206
|
+
hostKit,
|
|
207
|
+
createRegistries([{ Button }], { View: RNView }),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(result.counts.R).toBe(0);
|
|
211
|
+
expect(result.counts.K).toBe(0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('counts an app-authored memo wrapping a kit component exactly once', () => {
|
|
215
|
+
const innerButton = makeFiber({ type: Button });
|
|
216
|
+
const appMemo = link(
|
|
217
|
+
makeFiber({
|
|
218
|
+
tag: FIBER_TAG.MemoComponent,
|
|
219
|
+
type: { $$typeof: REACT_MEMO_TYPE, type: Button },
|
|
220
|
+
}),
|
|
221
|
+
[innerButton],
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const result = walkScreen(appMemo, createRegistries([{ Button }], {}));
|
|
225
|
+
|
|
226
|
+
expect(result.counts.K).toBe(1);
|
|
227
|
+
expect(result.kit.Button).toBe(1);
|
|
228
|
+
});
|
|
229
|
+
it('counts a kit element created by a mini-app bundle frame but not host-created children', () => {
|
|
230
|
+
const appStack = {
|
|
231
|
+
stack: [
|
|
232
|
+
'Error: react-stack-top-frame',
|
|
233
|
+
' at anonymous (http://localhost:8081/index.bundle:1:2)',
|
|
234
|
+
' at Tab (address at file:///x/Documents/apps/vn.momo.billpay/ios/a.bundle:1:2)',
|
|
235
|
+
' at react-stack-bottom-frame (http://localhost:8081/index.bundle:1:2)',
|
|
236
|
+
].join('\n'),
|
|
237
|
+
};
|
|
238
|
+
const hostStack = {
|
|
239
|
+
stack: [
|
|
240
|
+
'Error: react-stack-top-frame',
|
|
241
|
+
' at createElement (/repo/node_modules/react/index.js:1:1)',
|
|
242
|
+
' at Text (/repo/src/foundations/Text/index.tsx:10:5)',
|
|
243
|
+
' at react-stack-bottom-frame (/repo/node_modules/react/index.js:1:1)',
|
|
244
|
+
].join('\n'),
|
|
245
|
+
};
|
|
246
|
+
const inner = makeFiber({ type: Plain, _debugStack: hostStack });
|
|
247
|
+
const text = link(makeFiber({ type: Text, _debugStack: appStack }), [
|
|
248
|
+
inner,
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
const result = walkScreen(text, createRegistries([{ Text }], {}));
|
|
252
|
+
|
|
253
|
+
expect(result.counts).toEqual({ K: 1, R: 0, O: 0 });
|
|
254
|
+
expect(result.kit.Text).toBe(1);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('KitAdoption getCurrentFiber', () => {
|
|
259
|
+
it('returns the alternate when the fiber hangs off a stale HostRoot', () => {
|
|
260
|
+
const alternate = makeFiber({ type: Plain });
|
|
261
|
+
const fiber = makeFiber({ type: Plain, alternate });
|
|
262
|
+
const committedRoot = makeFiber({ tag: FIBER_TAG.HostRoot });
|
|
263
|
+
link(
|
|
264
|
+
makeFiber({
|
|
265
|
+
tag: FIBER_TAG.HostRoot,
|
|
266
|
+
stateNode: { current: committedRoot },
|
|
267
|
+
}),
|
|
268
|
+
[fiber],
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
expect(getCurrentFiber(fiber)).toBe(alternate);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('returns the fiber itself when its HostRoot is the committed one', () => {
|
|
275
|
+
const alternate = makeFiber({ type: Plain });
|
|
276
|
+
const fiber = makeFiber({ type: Plain, alternate });
|
|
277
|
+
const hostRoot = makeFiber({ tag: FIBER_TAG.HostRoot });
|
|
278
|
+
link(hostRoot, [fiber]);
|
|
279
|
+
hostRoot.stateNode = { current: hostRoot };
|
|
280
|
+
|
|
281
|
+
expect(getCurrentFiber(fiber)).toBe(fiber);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCreatorSource,
|
|
3
|
+
getSourceKey,
|
|
4
|
+
isAppAuthored,
|
|
5
|
+
parseFrameLocation,
|
|
6
|
+
} from '../KitAdoption/Authorship/utils';
|
|
7
|
+
import type { FiberLike } from '../KitAdoption/types';
|
|
8
|
+
|
|
9
|
+
const FACTORY =
|
|
10
|
+
' at anonymous (http://localhost:8081/index.bundle?platform=ios:221225:88)';
|
|
11
|
+
const BOTTOM =
|
|
12
|
+
' at react-stack-bottom-frame (http://localhost:8081/index.bundle?platform=ios:195108:33)';
|
|
13
|
+
const APP_DIR = 'file:///Users/x/Library/Documents/apps/vn.momo.billpay/ios';
|
|
14
|
+
const BILLPAY_STACK = [
|
|
15
|
+
'Error: react-stack-top-frame',
|
|
16
|
+
FACTORY,
|
|
17
|
+
` at BillPaymentTab (address at ${APP_DIR}/MainGroup.chunk.bundle:1:52311)`,
|
|
18
|
+
BOTTOM,
|
|
19
|
+
' at renderWithHooks (http://localhost:8081/index.bundle?platform=ios:188891:42)',
|
|
20
|
+
].join('\n');
|
|
21
|
+
const KIT_STACK = [
|
|
22
|
+
'Error: react-stack-top-frame',
|
|
23
|
+
' at anonymous (http://localhost:8081/index.bundle?platform=ios:220604:93)',
|
|
24
|
+
' at renderTitle (http://localhost:8081/index.bundle?platform=ios:290576:50)',
|
|
25
|
+
' at Button (http://localhost:8081/index.bundle?platform=ios:290744:32)',
|
|
26
|
+
BOTTOM,
|
|
27
|
+
].join('\n');
|
|
28
|
+
const HOST_PATH_STACK = [
|
|
29
|
+
'Error: react-stack-top-frame',
|
|
30
|
+
' at createElement (/repo/node_modules/react/index.js:1:1)',
|
|
31
|
+
' at Button (/repo/src/foundations/Button/index.tsx:10:5)',
|
|
32
|
+
BOTTOM,
|
|
33
|
+
].join('\n');
|
|
34
|
+
|
|
35
|
+
const makeFiber = (debugStack?: unknown): FiberLike => ({
|
|
36
|
+
tag: 0,
|
|
37
|
+
type: undefined,
|
|
38
|
+
_debugStack: debugStack,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('getSourceKey', () => {
|
|
42
|
+
it('returns the origin for http urls', () => {
|
|
43
|
+
expect(getSourceKey('http://localhost:8081/index.bundle?x=1:2:3')).toBe(
|
|
44
|
+
'http://localhost:8081',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns the directory for file urls', () => {
|
|
49
|
+
expect(getSourceKey('file:///a/b/c.bundle:1:2')).toBe('file:///a/b');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns local for bare paths', () => {
|
|
53
|
+
expect(getSourceKey('/repo/src/a.ts:1:2')).toBe('local');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('parseFrameLocation', () => {
|
|
58
|
+
it('parses the three frame shapes', () => {
|
|
59
|
+
expect(parseFrameLocation(' at fn (http://h:1/a.js:1:2)')).toBe(
|
|
60
|
+
'http://h:1/a.js:1:2',
|
|
61
|
+
);
|
|
62
|
+
expect(
|
|
63
|
+
parseFrameLocation(' at fn (address at file:///a/b.js:1:2)'),
|
|
64
|
+
).toBe('file:///a/b.js:1:2');
|
|
65
|
+
expect(parseFrameLocation(' at http://h:1/a.js:1:2')).toBe(
|
|
66
|
+
'http://h:1/a.js:1:2',
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('getCreatorSource', () => {
|
|
72
|
+
it('returns the mini-app directory for an app-created element', () => {
|
|
73
|
+
expect(getCreatorSource({ stack: BILLPAY_STACK })).toBe(APP_DIR);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns the host origin for a kit-created element', () => {
|
|
77
|
+
expect(getCreatorSource({ stack: KIT_STACK })).toBe(
|
|
78
|
+
'http://localhost:8081',
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('skips a jsxDEV wrapper frame', () => {
|
|
83
|
+
const stack = [
|
|
84
|
+
'Error: react-stack-top-frame',
|
|
85
|
+
FACTORY,
|
|
86
|
+
' at jsxDEV (http://localhost:8081/index.bundle:1:2)',
|
|
87
|
+
` at Screen (address at ${APP_DIR}/a.bundle:1:2)`,
|
|
88
|
+
BOTTOM,
|
|
89
|
+
].join('\n');
|
|
90
|
+
expect(getCreatorSource({ stack })).toBe(APP_DIR);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('skips a native frame', () => {
|
|
94
|
+
const stack = [
|
|
95
|
+
'Error: react-stack-top-frame',
|
|
96
|
+
FACTORY,
|
|
97
|
+
' at apply (native)',
|
|
98
|
+
` at Screen (address at ${APP_DIR}/a.bundle:1:2)`,
|
|
99
|
+
BOTTOM,
|
|
100
|
+
].join('\n');
|
|
101
|
+
expect(getCreatorSource({ stack })).toBe(APP_DIR);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns undefined without a bottom marker', () => {
|
|
105
|
+
const stack = ['Error: x', FACTORY].join('\n');
|
|
106
|
+
expect(getCreatorSource({ stack })).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns undefined for non-error and string values', () => {
|
|
110
|
+
expect(getCreatorSource('dev-stack')).toBeUndefined();
|
|
111
|
+
expect(getCreatorSource(42)).toBeUndefined();
|
|
112
|
+
expect(getCreatorSource(null)).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('isAppAuthored', () => {
|
|
117
|
+
it('is true without a debug stack', () => {
|
|
118
|
+
expect(isAppAuthored(makeFiber())).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('is true when the creator frame is in another bundle', () => {
|
|
122
|
+
expect(isAppAuthored(makeFiber({ stack: BILLPAY_STACK }))).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('is false when the creator frame is a host source path', () => {
|
|
126
|
+
expect(isAppAuthored(makeFiber({ stack: HOST_PATH_STACK }))).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('is false for an unparseable stack', () => {
|
|
130
|
+
expect(isAppAuthored(makeFiber('dev-stack'))).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|