react-native-windows 0.82.3 โ 0.83.0-preview.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.
- package/.flowconfig +2 -7
- package/Libraries/Animated/components/AnimatedFlatList.js +5 -3
- package/Libraries/Animated/components/AnimatedImage.js +4 -3
- package/Libraries/Animated/components/AnimatedSectionList.js +2 -2
- package/Libraries/Animated/components/AnimatedText.js +7 -3
- package/Libraries/Animated/components/AnimatedView.js +3 -2
- package/Libraries/Animated/createAnimatedComponent.js +24 -12
- package/Libraries/Animated/nodes/AnimatedColor.js +26 -10
- package/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js +43 -15
- package/Libraries/Components/AccessibilityInfo/AccessibilityInfo.windows.js +43 -15
- package/Libraries/Components/ActivityIndicator/ActivityIndicator.d.ts +2 -2
- package/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.d.ts +2 -2
- package/Libraries/Components/Glyph/Glyph.js +4 -1
- package/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.d.ts +2 -2
- package/Libraries/Components/RefreshControl/RefreshControl.d.ts +3 -3
- package/Libraries/Components/SafeAreaView/SafeAreaView.d.ts +2 -2
- package/Libraries/Components/ScrollView/ScrollView.js +1 -0
- package/Libraries/Components/ScrollView/ScrollView.windows.js +1 -0
- package/Libraries/Components/ScrollView/ScrollViewStickyHeader.js +6 -6
- package/Libraries/Components/Switch/Switch.d.ts +2 -2
- package/Libraries/Components/Switch/Switch.windows.js +1 -1
- package/Libraries/Components/TextInput/TextInput.d.ts +2 -5
- package/Libraries/Components/TextInput/TextInput.js +6 -0
- package/Libraries/Components/TextInput/TextInput.windows.js +6 -0
- package/Libraries/Components/Touchable/TouchableBounce.js +7 -7
- package/Libraries/Components/Touchable/TouchableBounce.windows.js +7 -7
- package/Libraries/Components/Touchable/TouchableWithoutFeedback.windows.js +1 -1
- package/Libraries/Components/View/ReactNativeStyleAttributes.js +19 -1
- package/Libraries/Components/View/View.d.ts +2 -2
- package/Libraries/Components/View/View.windows.js +0 -1
- package/Libraries/Components/View/ViewNativeComponent.js +13 -1
- package/Libraries/Core/ReactNativeVersion.js +3 -3
- package/Libraries/Core/setUpPerformance.js +2 -0
- package/Libraries/Debugging/DebuggingOverlay.js +14 -14
- package/Libraries/Debugging/DebuggingOverlayRegistry.js +8 -2
- package/Libraries/EventEmitter/RCTDeviceEventEmitter.js +5 -2
- package/Libraries/Image/Image.d.ts +3 -3
- package/Libraries/Image/ImageInjection.js +3 -6
- package/Libraries/Image/ImageTypes.flow.js +3 -7
- package/Libraries/Lists/FlatList.js +8 -8
- package/Libraries/Lists/SectionList.d.ts +5 -1
- package/Libraries/Lists/ViewabilityHelper.js +1 -1
- package/Libraries/Lists/VirtualizedList.js +1 -0
- package/Libraries/LogBox/UI/AnsiHighlight.js +4 -1
- package/Libraries/NativeComponent/BaseViewConfig.android.js +11 -2
- package/Libraries/NativeComponent/NativeComponentRegistry.d.ts +98 -0
- package/Libraries/NativeComponent/NativeComponentRegistry.js +2 -0
- package/Libraries/NativeComponent/NativeComponentRegistryUnstable.js +3 -1
- package/Libraries/NativeComponent/ViewConfigIgnore.windows.js +45 -0
- package/Libraries/Network/RCTNetworking.android.js +3 -1
- package/Libraries/Network/RCTNetworking.ios.js +3 -0
- package/Libraries/Network/RCTNetworking.windows.js +3 -0
- package/Libraries/Network/XMLHttpRequest.js +1 -41
- package/Libraries/Pressability/usePressability.js +14 -3
- package/Libraries/ReactNative/PaperUIManager.windows.js +3 -3
- package/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js +20 -82
- package/Libraries/ReactNative/getNativeComponentAttributes.js +12 -0
- package/Libraries/Renderer/implementations/ReactFabric-dev.js +6759 -4478
- package/Libraries/Renderer/implementations/ReactFabric-prod.js +3169 -3119
- package/Libraries/Renderer/implementations/ReactFabric-profiling.js +4732 -3535
- package/Libraries/Renderer/implementations/ReactNativeRenderer-dev.js +6646 -4070
- package/Libraries/Renderer/implementations/ReactNativeRenderer-prod.js +3136 -2825
- package/Libraries/Renderer/implementations/ReactNativeRenderer-profiling.js +4761 -3312
- package/Libraries/Renderer/shims/ReactNative.js +3 -1
- package/Libraries/Renderer/shims/ReactNativeTypes.js +2 -1
- package/Libraries/Renderer/shims/ReactNativeTypes.windows.js +2 -1
- package/Libraries/StyleSheet/StyleSheetTypes.d.ts +1 -1
- package/Libraries/StyleSheet/StyleSheetTypes.js +44 -0
- package/Libraries/StyleSheet/processBackgroundPosition.js +284 -0
- package/Libraries/StyleSheet/processBackgroundRepeat.js +105 -0
- package/Libraries/StyleSheet/processBackgroundSize.js +104 -0
- package/Libraries/Text/Text.d.ts +2 -2
- package/Libraries/Text/TextNativeComponent.js +10 -0
- package/Libraries/TurboModule/TurboModuleRegistry.js +3 -9
- package/Libraries/Utilities/DevLoadingView.js +14 -6
- package/Libraries/Utilities/HMRClient.js +13 -5
- package/Microsoft.ReactNative/CompositionComponentView.idl +2 -0
- package/Microsoft.ReactNative/CompositionHwndHost.idl +1 -0
- package/Microsoft.ReactNative/Fabric/ComponentView.cpp +1 -1
- package/Microsoft.ReactNative/Fabric/ComponentView.h +1 -1
- package/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +2 -52
- package/Microsoft.ReactNative/Fabric/Composition/CompositionHwndHost.cpp +10 -40
- package/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +3 -80
- package/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +45 -12
- package/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h +8 -0
- package/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp +61 -74
- package/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.h +4 -0
- package/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h +1 -0
- package/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp +4 -3
- package/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h +2 -1
- package/Microsoft.ReactNative/Fabric/Composition/ReactNativeWindow.cpp +245 -0
- package/Microsoft.ReactNative/Fabric/Composition/ReactNativeWindow.h +80 -0
- package/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentDescriptor.h +20 -36
- package/Microsoft.ReactNative/Fabric/Composition/UriImageManager.cpp +5 -0
- package/Microsoft.ReactNative/IReactCompositionViewComponentBuilder.idl +1 -0
- package/Microsoft.ReactNative/Modules/ImageViewManagerModule.cpp +1 -1
- package/Microsoft.ReactNative/Modules/LogBoxModule.cpp +20 -94
- package/Microsoft.ReactNative/Modules/LogBoxModule.h +1 -3
- package/Microsoft.ReactNative/ReactNativeAppBuilder.cpp +0 -41
- package/Microsoft.ReactNative/ReactNativeAppBuilder.idl +0 -11
- package/Microsoft.ReactNative/ReactNativeIsland.idl +2 -3
- package/Microsoft.ReactNative/ReactNativeWin32App.cpp +31 -101
- package/Microsoft.ReactNative/ReactNativeWin32App.h +2 -13
- package/Microsoft.ReactNative/ReactNativeWindow.idl +44 -0
- package/Microsoft.ReactNative.Cxx/AutoDraw.h +9 -1
- package/Microsoft.ReactNative.Cxx/ReactCommon/CallInvoker.h +13 -16
- package/Microsoft.ReactNative.Cxx/ReactCommon/TurboModule.h +24 -36
- package/Microsoft.ReactNative.Cxx/ReactCommon/TurboModuleUtils.h +5 -8
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/AString.h +8 -6
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Array.h +32 -49
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Base.h +27 -76
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Bool.h +4 -2
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/CallbackWrapper.h +19 -18
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Class.h +25 -48
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Convert.h +38 -31
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Error.h +11 -6
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/EventEmitter.h +47 -45
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Function.h +69 -89
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/HighResTimeStamp.h +8 -8
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/LongLivedObject.h +6 -6
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Number.h +16 -8
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Object.h +17 -24
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Promise.h +17 -17
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/bridging/Value.h +20 -29
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/debug/react_native_assert.h +2 -7
- package/Microsoft.ReactNative.Cxx/ReactCommon/react/timing/primitives.h +127 -115
- package/PropertySheets/Generated/PackageVersion.g.props +5 -5
- package/PropertySheets/Warnings.props +2 -1
- package/ReactCommon/ReactCommon.vcxproj +11 -2
- package/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/CxxNativeModule.cpp +2 -2
- package/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/Instance.cpp +4 -2
- package/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/JSExecutor.cpp +4 -2
- package/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/JSIndexedRAMBundle.cpp +4 -2
- package/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/MethodCall.cpp +4 -2
- package/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/ModuleRegistry.cpp +5 -3
- package/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/NativeToJsBridge.cpp +4 -4
- package/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/RAMBundleRegistry.cpp +6 -4
- package/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/ReactMarker.cpp +2 -0
- package/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/TraceSection.h +25 -35
- package/ReactCommon/TEMP_UntilReactCommonUpdate/jsi/jsi/test/testlib.cpp +86 -67
- package/ReactCommon/TEMP_UntilReactCommonUpdate/jsiexecutor/jsireact/JSIExecutor.cpp +15 -8
- package/ReactCommon/TEMP_UntilReactCommonUpdate/jsiexecutor/jsireact/JSINativeModules.cpp +4 -2
- package/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.cpp +84 -68
- package/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.h +31 -35
- package/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/Utf8.h +4 -5
- package/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/network/HttpUtils.cpp +2 -1
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/nativemodule/core/ReactCommon/TurboModule.h +24 -36
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/nativemodule/dom/NativeDOM.h +28 -66
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/nativemodule/webperformance/NativePerformance.cpp +5 -0
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/componentregistry/ComponentDescriptorRegistry.cpp +3 -3
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/image/conversions.h +8 -4
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/text/ParagraphShadowNode.cpp +19 -16
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/text/platform/android/react/renderer/components/text/ParagraphState.h +8 -9
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/AccessibilityPrimitives.h +25 -95
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/accessibilityPropsConversions.h +85 -42
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/core/EventDispatcher.cpp +2 -0
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/core/EventQueueProcessor.cpp +2 -0
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/imagemanager/primitives.h +25 -31
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/uimanager/UIManager.cpp +19 -5
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/runtime/ReactInstance.cpp +16 -1
- package/Scripts/Tfs/Layout-Desktop-Headers.ps1 +1 -15
- package/Scripts/creaternwapp.cmd +1 -1
- package/Scripts/perf/compare-results.js +357 -0
- package/Scripts/perf/create-perf-test.js +343 -0
- package/Scripts/perf/post-pr-comment.js +210 -0
- package/Shared/Hermes/HermesRuntimeTargetDelegate.cpp +8 -0
- package/Shared/Hermes/HermesRuntimeTargetDelegate.h +3 -0
- package/Shared/Shared.vcxitems +15 -3
- package/Shared/Shared.vcxitems.filters +11 -3
- package/codegen/NativeIntersectionObserverSpec.g.h +2 -0
- package/codegen/NativeNetworkingIOSSpec.g.h +2 -0
- package/codegen/NativePerformanceSpec.g.h +6 -0
- package/codegen/NativeReactNativeFeatureFlagsSpec.g.h +229 -139
- package/codegen/react/components/rnwcore/ActivityIndicatorView.g.h +2 -1
- package/codegen/react/components/rnwcore/AndroidDrawerLayout.g.h +42 -25
- package/codegen/react/components/rnwcore/AndroidHorizontalScrollContentView.g.h +2 -1
- package/codegen/react/components/rnwcore/AndroidProgressBar.g.h +2 -1
- package/codegen/react/components/rnwcore/AndroidSwipeRefreshLayout.g.h +11 -6
- package/codegen/react/components/rnwcore/AndroidSwitch.g.h +11 -6
- package/codegen/react/components/rnwcore/DebuggingOverlay.g.h +1 -0
- package/codegen/react/components/rnwcore/InputAccessory.g.h +2 -1
- package/codegen/react/components/rnwcore/ModalHostView.g.h +40 -23
- package/codegen/react/components/rnwcore/Props.cpp +6 -1
- package/codegen/react/components/rnwcore/Props.h +1 -0
- package/codegen/react/components/rnwcore/PullToRefreshView.g.h +11 -6
- package/codegen/react/components/rnwcore/SafeAreaView.g.h +1 -0
- package/codegen/react/components/rnwcore/Switch.g.h +11 -6
- package/codegen/react/components/rnwcore/UnimplementedNativeView.g.h +2 -1
- package/codegen/react/components/rnwcore/VirtualView.g.h +41 -8
- package/codegen/react/components/rnwcore/VirtualViewExperimental.g.h +45 -8
- package/codegen/rnwcoreJSI.h +3973 -6059
- package/index.js +6 -0
- package/index.windows.js +6 -0
- package/jest/mockComponent.js +6 -6
- package/jest/setup.js +15 -10
- package/package.json +27 -27
- package/src/private/components/virtualview/VirtualView.js +22 -27
- package/src/private/components/virtualview/VirtualViewExperimentalNativeComponent.js +6 -0
- package/src/private/featureflags/ReactNativeFeatureFlags.js +100 -19
- package/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +18 -3
- package/src/private/setup/setUpDefaultReactNativeEnvironment.js +6 -0
- package/src/private/specs_DEPRECATED/components/SwitchNativeComponent.js +1 -1
- package/src/private/specs_DEPRECATED/modules/NativeNetworkingIOS.js +1 -0
- package/src/private/webapis/dom/nodes/ReactNativeElement.js +12 -2
- package/src/private/webapis/intersectionobserver/IntersectionObserver.js +76 -15
- package/src/private/webapis/intersectionobserver/internals/IntersectionObserverManager.js +1 -0
- package/src/private/webapis/intersectionobserver/specs/NativeIntersectionObserver.js +1 -0
- package/src/private/webapis/performance/ResourceTiming.js +31 -4
- package/src/private/webapis/performance/internals/RawPerformanceEntry.js +4 -1
- package/src/private/webapis/performance/specs/NativePerformance.js +3 -0
- package/stubs/double-conversion/double-conversion.h +5 -0
- package/templates/cpp-app/template.config.js +1 -1
- package/templates/cpp-app/windows/MyApp/MyApp.vcxproj +1 -1
- package/templates/cpp-lib/template.config.js +1 -1
- package/templates/cpp-lib/windows/MyLib/MyLib.vcxproj +1 -1
- package/types/index.d.ts +1 -0
- package/types/public/ReactNativeTypes.d.ts +115 -2
- package/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js +0 -152
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/BaseViewProps.cpp +0 -628
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/conversions.h +0 -1574
- package/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/core/ShadowNode.cpp +0 -528
- package/Scripts/OpenSSL.nuspec +0 -39
- package/Scripts/OpenSSL.targets +0 -36
- package/codegen/rnwcoreJSI-generated.cpp +0 -3470
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*
|
|
5
|
+
* Compare perf test results from the current (head) run against
|
|
6
|
+
* committed baselines from the base branch. Produces a markdown
|
|
7
|
+
* comparison report and exits with non-zero if regressions detected.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node vnext/Scripts/perf/compare-results.js [options]
|
|
11
|
+
*
|
|
12
|
+
* Options:
|
|
13
|
+
* --results <path> Path to CI results JSON (default: .perf-results/results.json)
|
|
14
|
+
* --baselines <dir> Path to base branch perf snapshots directory
|
|
15
|
+
* --output <path> Path to write markdown report (default: .perf-results/report.md)
|
|
16
|
+
* --fail-on-regression Exit 1 if regressions found (default: true in CI)
|
|
17
|
+
*
|
|
18
|
+
* @format
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
function parseArgs() {
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
const opts = {
|
|
29
|
+
results: '.perf-results/results.json',
|
|
30
|
+
baselines: null, // auto-detect from test file paths
|
|
31
|
+
output: '.perf-results/report.md',
|
|
32
|
+
failOnRegression: !!process.env.CI,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < args.length; i++) {
|
|
36
|
+
switch (args[i]) {
|
|
37
|
+
case '--results':
|
|
38
|
+
opts.results = args[++i];
|
|
39
|
+
break;
|
|
40
|
+
case '--baselines':
|
|
41
|
+
opts.baselines = args[++i];
|
|
42
|
+
break;
|
|
43
|
+
case '--output':
|
|
44
|
+
opts.output = args[++i];
|
|
45
|
+
break;
|
|
46
|
+
case '--fail-on-regression':
|
|
47
|
+
opts.failOnRegression = true;
|
|
48
|
+
break;
|
|
49
|
+
case '--no-fail-on-regression':
|
|
50
|
+
opts.failOnRegression = false;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return opts;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getSnapshotPath(testFilePath) {
|
|
58
|
+
const dir = path.dirname(testFilePath);
|
|
59
|
+
const basename = path.basename(testFilePath);
|
|
60
|
+
return path.join(dir, '__perf_snapshots__', `${basename}.perf-baseline.json`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function loadSnapshot(filePath) {
|
|
64
|
+
if (fs.existsSync(filePath)) {
|
|
65
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
66
|
+
}
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const DEFAULT_THRESHOLD = {
|
|
71
|
+
maxDurationIncrease: 10,
|
|
72
|
+
maxDuration: Infinity,
|
|
73
|
+
minAbsoluteDelta: 3,
|
|
74
|
+
maxRenderCount: 5,
|
|
75
|
+
minRuns: 10,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function resolveThreshold(threshold) {
|
|
79
|
+
return {
|
|
80
|
+
...DEFAULT_THRESHOLD,
|
|
81
|
+
...threshold,
|
|
82
|
+
// JSON.stringify(Infinity) === 'null', so restore it
|
|
83
|
+
maxDuration:
|
|
84
|
+
threshold.maxDuration === null || threshold.maxDuration === undefined
|
|
85
|
+
? Infinity
|
|
86
|
+
: threshold.maxDuration,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function compareEntry(head, base, threshold) {
|
|
91
|
+
// median for comparison
|
|
92
|
+
const percentChange =
|
|
93
|
+
base.medianDuration > 0
|
|
94
|
+
? ((head.medianDuration - base.medianDuration) / base.medianDuration) *
|
|
95
|
+
100
|
|
96
|
+
: head.medianDuration > 0
|
|
97
|
+
? Infinity
|
|
98
|
+
: 0;
|
|
99
|
+
|
|
100
|
+
const errors = [];
|
|
101
|
+
|
|
102
|
+
const absoluteDelta = head.medianDuration - base.medianDuration;
|
|
103
|
+
const minAbsoluteDelta =
|
|
104
|
+
threshold.minAbsoluteDelta ?? DEFAULT_THRESHOLD.minAbsoluteDelta;
|
|
105
|
+
if (
|
|
106
|
+
percentChange > threshold.maxDurationIncrease &&
|
|
107
|
+
absoluteDelta > minAbsoluteDelta
|
|
108
|
+
) {
|
|
109
|
+
errors.push(
|
|
110
|
+
`Duration increased by ${percentChange.toFixed(1)}% / +${absoluteDelta.toFixed(2)}ms (threshold: ${threshold.maxDurationIncrease}% & ${minAbsoluteDelta}ms)`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (head.medianDuration > threshold.maxDuration) {
|
|
114
|
+
errors.push(
|
|
115
|
+
`Duration ${head.medianDuration.toFixed(2)}ms exceeds max ${threshold.maxDuration}ms`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (head.renderCount > threshold.maxRenderCount) {
|
|
119
|
+
errors.push(
|
|
120
|
+
`Render count ${head.renderCount} exceeds max ${threshold.maxRenderCount}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
name: head.name,
|
|
126
|
+
head,
|
|
127
|
+
base,
|
|
128
|
+
percentChange,
|
|
129
|
+
passed: errors.length === 0,
|
|
130
|
+
errors,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function compareSuite(suiteName, headSnaps, baseSnaps) {
|
|
135
|
+
const results = [];
|
|
136
|
+
|
|
137
|
+
for (const [key, headEntry] of Object.entries(headSnaps)) {
|
|
138
|
+
const baseEntry = baseSnaps[key];
|
|
139
|
+
const threshold = resolveThreshold({
|
|
140
|
+
...DEFAULT_THRESHOLD,
|
|
141
|
+
...(headEntry.threshold || {}),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!baseEntry) {
|
|
145
|
+
results.push({
|
|
146
|
+
name: headEntry.metrics.name,
|
|
147
|
+
head: headEntry.metrics,
|
|
148
|
+
base: null,
|
|
149
|
+
percentChange: null,
|
|
150
|
+
passed: true,
|
|
151
|
+
errors: [],
|
|
152
|
+
isNew: true,
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
results.push(compareEntry(headEntry.metrics, baseEntry.metrics, threshold));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Detect removed scenarios
|
|
161
|
+
for (const key of Object.keys(baseSnaps)) {
|
|
162
|
+
if (!headSnaps[key]) {
|
|
163
|
+
results.push({
|
|
164
|
+
name: `[REMOVED] ${baseSnaps[key].metrics.name}`,
|
|
165
|
+
head: null,
|
|
166
|
+
base: baseSnaps[key].metrics,
|
|
167
|
+
percentChange: null,
|
|
168
|
+
passed: true,
|
|
169
|
+
errors: [],
|
|
170
|
+
isRemoved: true,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {suiteName, results, hasRegressions: results.some(r => !r.passed)};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function generateMarkdown(suiteComparisons, ciResults) {
|
|
179
|
+
let md = '## Performance Test Results\n\n';
|
|
180
|
+
|
|
181
|
+
md += `**Branch:** \`${ciResults.branch}\` \n`;
|
|
182
|
+
md += `**Commit:** \`${ciResults.commitSha.slice(0, 8)}\` \n`;
|
|
183
|
+
md += `**Time:** ${ciResults.timestamp} \n`;
|
|
184
|
+
md += `**Tests:** ${ciResults.summary.passed}/${ciResults.summary.totalTests} passed \n\n`;
|
|
185
|
+
|
|
186
|
+
const allRegressions = suiteComparisons.filter(s => s.hasRegressions);
|
|
187
|
+
if (allRegressions.length > 0) {
|
|
188
|
+
md += '### โ Regressions Detected\n\n';
|
|
189
|
+
for (const suite of allRegressions) {
|
|
190
|
+
md += `#### ${suite.suiteName}\n\n`;
|
|
191
|
+
md += '| Scenario | Baseline | Current | Change | Status |\n';
|
|
192
|
+
md += '|----------|----------|---------|--------|--------|\n';
|
|
193
|
+
for (const r of suite.results.filter(x => !x.passed)) {
|
|
194
|
+
const baseline = r.base ? `${r.base.meanDuration.toFixed(2)}ms` : 'N/A';
|
|
195
|
+
const current = r.head ? `${r.head.meanDuration.toFixed(2)}ms` : 'N/A';
|
|
196
|
+
const change =
|
|
197
|
+
r.percentChange != null ? `+${r.percentChange.toFixed(1)}%` : 'N/A';
|
|
198
|
+
md += `| ${r.name} | ${baseline} | ${current} | ${change} | โ |\n`;
|
|
199
|
+
}
|
|
200
|
+
md += '\n';
|
|
201
|
+
for (const r of suite.results.filter(x => !x.passed)) {
|
|
202
|
+
if (r.errors.length > 0) {
|
|
203
|
+
md += `> **${r.name}:** ${r.errors.join('; ')} \n`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
md += '\n';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Passed suites
|
|
211
|
+
const passedSuites = suiteComparisons.filter(s => !s.hasRegressions);
|
|
212
|
+
if (passedSuites.length > 0) {
|
|
213
|
+
md += '### โ
Passed\n\n';
|
|
214
|
+
md += '<details>\n<summary>';
|
|
215
|
+
const totalPassed = passedSuites.reduce(
|
|
216
|
+
(acc, s) => acc + s.results.length,
|
|
217
|
+
0,
|
|
218
|
+
);
|
|
219
|
+
md += `${totalPassed} scenario(s) across ${passedSuites.length} suite(s) โ no regressions`;
|
|
220
|
+
md += '</summary>\n\n';
|
|
221
|
+
|
|
222
|
+
for (const suite of passedSuites) {
|
|
223
|
+
md += `#### ${suite.suiteName}\n\n`;
|
|
224
|
+
md += '| Scenario | Mean | Median | StdDev | Renders | vs Baseline |\n';
|
|
225
|
+
md += '|----------|------|--------|--------|---------|-------------|\n';
|
|
226
|
+
for (const r of suite.results) {
|
|
227
|
+
if (!r.head) continue;
|
|
228
|
+
const change =
|
|
229
|
+
r.percentChange != null
|
|
230
|
+
? `${r.percentChange >= 0 ? '+' : ''}${r.percentChange.toFixed(1)}%`
|
|
231
|
+
: 'new';
|
|
232
|
+
md +=
|
|
233
|
+
`| ${r.name}` +
|
|
234
|
+
` | ${r.head.meanDuration.toFixed(2)}ms` +
|
|
235
|
+
` | ${r.head.medianDuration.toFixed(2)}ms` +
|
|
236
|
+
` | ยฑ${r.head.stdDev.toFixed(2)}ms` +
|
|
237
|
+
` | ${r.head.renderCount}` +
|
|
238
|
+
` | ${change} |\n`;
|
|
239
|
+
}
|
|
240
|
+
md += '\n';
|
|
241
|
+
}
|
|
242
|
+
md += '</details>\n';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// New scenarios
|
|
246
|
+
const allNew = suiteComparisons.flatMap(s => s.results.filter(r => r.isNew));
|
|
247
|
+
if (allNew.length > 0) {
|
|
248
|
+
md += '\n### ๐ New Scenarios\n\n';
|
|
249
|
+
md += `${allNew.length} new scenario(s) added (no baseline comparison). \n`;
|
|
250
|
+
md +=
|
|
251
|
+
'These will become baselines after merge. Run `yarn perf:update` to capture locally.\n';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return md;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function main() {
|
|
258
|
+
const opts = parseArgs();
|
|
259
|
+
|
|
260
|
+
// 1. Load CI results JSON
|
|
261
|
+
if (!fs.existsSync(opts.results)) {
|
|
262
|
+
console.error(`โ Results file not found: ${opts.results}`);
|
|
263
|
+
console.error('Run perf tests with CI=true first: CI=true yarn perf:ci');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const ciResults = JSON.parse(fs.readFileSync(opts.results, 'utf-8'));
|
|
268
|
+
console.log(
|
|
269
|
+
`๐ Loaded ${ciResults.suites.length} suite(s) from ${opts.results}`,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// 2. Compare each suite against its committed baseline
|
|
273
|
+
const suiteComparisons = [];
|
|
274
|
+
|
|
275
|
+
for (const suite of ciResults.suites) {
|
|
276
|
+
// Head snapshots come from the CI run
|
|
277
|
+
const headSnaps = suite.snapshots || {};
|
|
278
|
+
|
|
279
|
+
// Base snapshots come from the committed .perf.snap files
|
|
280
|
+
// In CI, the base branch is checked out into a separate dir,
|
|
281
|
+
// or we read the committed snapshots from the test file path
|
|
282
|
+
let baseSnapPath;
|
|
283
|
+
if (opts.baselines) {
|
|
284
|
+
// Explicit baseline directory โ look for matching snap file
|
|
285
|
+
const relPath = path.relative(process.cwd(), suite.testFilePath);
|
|
286
|
+
baseSnapPath = path.join(
|
|
287
|
+
opts.baselines,
|
|
288
|
+
path.dirname(relPath),
|
|
289
|
+
'__perf_snapshots__',
|
|
290
|
+
`${path.basename(suite.testFilePath)}.perf.snap`,
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
// Default: baselines live adjacent to test files (committed in repo)
|
|
294
|
+
baseSnapPath = getSnapshotPath(suite.testFilePath);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const baseSnaps = loadSnapshot(baseSnapPath);
|
|
298
|
+
console.log(
|
|
299
|
+
` ๐ ${suite.suiteName}: ${Object.keys(headSnaps).length} head / ${Object.keys(baseSnaps).length} base snapshots`,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
suiteComparisons.push(compareSuite(suite.suiteName, headSnaps, baseSnaps));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 3. Generate markdown report
|
|
306
|
+
const markdown = generateMarkdown(suiteComparisons, ciResults);
|
|
307
|
+
|
|
308
|
+
const outputDir = path.dirname(opts.output);
|
|
309
|
+
if (!fs.existsSync(outputDir)) {
|
|
310
|
+
fs.mkdirSync(outputDir, {recursive: true});
|
|
311
|
+
}
|
|
312
|
+
fs.writeFileSync(opts.output, markdown, 'utf-8');
|
|
313
|
+
console.log(`\n๐ Report written to ${opts.output}`);
|
|
314
|
+
|
|
315
|
+
// 4. Also write a machine-readable summary
|
|
316
|
+
const summaryPath = opts.output.replace(/\.md$/, '.json');
|
|
317
|
+
const summaryData = {
|
|
318
|
+
hasRegressions: suiteComparisons.some(s => s.hasRegressions),
|
|
319
|
+
totalSuites: suiteComparisons.length,
|
|
320
|
+
totalScenarios: suiteComparisons.reduce(
|
|
321
|
+
(acc, s) => acc + s.results.length,
|
|
322
|
+
0,
|
|
323
|
+
),
|
|
324
|
+
regressions: suiteComparisons.flatMap(s =>
|
|
325
|
+
s.results
|
|
326
|
+
.filter(r => !r.passed)
|
|
327
|
+
.map(r => ({
|
|
328
|
+
suite: s.suiteName,
|
|
329
|
+
scenario: r.name,
|
|
330
|
+
percentChange: r.percentChange,
|
|
331
|
+
errors: r.errors,
|
|
332
|
+
})),
|
|
333
|
+
),
|
|
334
|
+
};
|
|
335
|
+
fs.writeFileSync(summaryPath, JSON.stringify(summaryData, null, 2) + '\n');
|
|
336
|
+
|
|
337
|
+
// 5. Print summary
|
|
338
|
+
const hasRegressions = suiteComparisons.some(s => s.hasRegressions);
|
|
339
|
+
if (hasRegressions) {
|
|
340
|
+
const count = summaryData.regressions.length;
|
|
341
|
+
console.error(`\nโ ${count} regression(s) detected!`);
|
|
342
|
+
for (const reg of summaryData.regressions) {
|
|
343
|
+
console.error(
|
|
344
|
+
` - ${reg.suite} / ${reg.scenario}: ${reg.errors.join('; ')}`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (opts.failOnRegression) {
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
console.log(
|
|
352
|
+
`\nโ
All ${summaryData.totalScenarios} scenarios passed โ no regressions.`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
main();
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*
|
|
6
|
+
* Interactive CLI to scaffold a new component performance test.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node vnext/Scripts/perf/create-perf-test.js
|
|
10
|
+
* yarn perf:create (from e2e-test-app-fabric)
|
|
11
|
+
*
|
|
12
|
+
* Generates a `.perf-test.tsx` file with:
|
|
13
|
+
* - Class extending ComponentPerfTestBase
|
|
14
|
+
* - Standard mount / unmount / rerender tests
|
|
15
|
+
* - Placeholder for custom scenarios
|
|
16
|
+
* - Correct imports, test ID, and category
|
|
17
|
+
*
|
|
18
|
+
* @format
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const readline = require('readline');
|
|
24
|
+
|
|
25
|
+
function ask(rl, question, defaultValue) {
|
|
26
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
27
|
+
return new Promise(resolve => {
|
|
28
|
+
rl.question(`${question}${suffix}: `, answer => {
|
|
29
|
+
resolve(answer.trim() || defaultValue || '');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function choose(rl, question, options, defaultIndex = 0) {
|
|
35
|
+
return new Promise(resolve => {
|
|
36
|
+
console.log(`\n${question}`);
|
|
37
|
+
options.forEach((opt, i) => {
|
|
38
|
+
const marker = i === defaultIndex ? ' (default)' : '';
|
|
39
|
+
console.log(` ${i + 1}. ${opt}${marker}`);
|
|
40
|
+
});
|
|
41
|
+
rl.question(`Choice [${defaultIndex + 1}]: `, answer => {
|
|
42
|
+
const idx = parseInt(answer, 10) - 1;
|
|
43
|
+
resolve(
|
|
44
|
+
idx >= 0 && idx < options.length ? options[idx] : options[defaultIndex],
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toPascalCase(str) {
|
|
51
|
+
return str
|
|
52
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
53
|
+
.replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toKebabCase(str) {
|
|
57
|
+
return str
|
|
58
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
59
|
+
.replace(/[\s_]+/g, '-')
|
|
60
|
+
.toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Split a props string into individual prop tokens,
|
|
65
|
+
* correctly handling nested braces like onPress={() => {}}.
|
|
66
|
+
*/
|
|
67
|
+
function splitProps(propsStr) {
|
|
68
|
+
const props = [];
|
|
69
|
+
let current = '';
|
|
70
|
+
let braceDepth = 0;
|
|
71
|
+
let inString = null; // '"' or "'"
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < propsStr.length; i++) {
|
|
74
|
+
const ch = propsStr[i];
|
|
75
|
+
|
|
76
|
+
// Track string boundaries
|
|
77
|
+
if ((ch === '"' || ch === "'") && !inString) {
|
|
78
|
+
inString = ch;
|
|
79
|
+
} else if (ch === inString) {
|
|
80
|
+
inString = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!inString) {
|
|
84
|
+
if (ch === '{') braceDepth++;
|
|
85
|
+
if (ch === '}') braceDepth--;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Split on whitespace only when outside braces/strings
|
|
89
|
+
if (ch === ' ' && braceDepth === 0 && !inString) {
|
|
90
|
+
if (current.trim()) props.push(current.trim());
|
|
91
|
+
current = '';
|
|
92
|
+
} else {
|
|
93
|
+
current += ch;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (current.trim()) props.push(current.trim());
|
|
97
|
+
return props;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const CATEGORIES = ['core', 'extended', 'interactive', 'list', 'community'];
|
|
101
|
+
|
|
102
|
+
const PERF_TEST_ROOT = path.resolve(
|
|
103
|
+
__dirname,
|
|
104
|
+
'../../..',
|
|
105
|
+
'packages/e2e-test-app-fabric/test/__perf__',
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Known required props for common RN components.
|
|
109
|
+
// These are auto-applied so the generated test works out of the box.
|
|
110
|
+
const KNOWN_REQUIRED_PROPS = {
|
|
111
|
+
Button: 'title="Perf Test" onPress={() => {}}',
|
|
112
|
+
Switch: 'value={false}',
|
|
113
|
+
Image: 'source={{uri: "https://example.com/img.png"}}',
|
|
114
|
+
FlatList: 'data={[]} renderItem={() => null}',
|
|
115
|
+
SectionList: 'sections={[]} renderItem={() => null}',
|
|
116
|
+
Modal: 'visible={true}',
|
|
117
|
+
TouchableHighlight: 'onPress={() => {}}',
|
|
118
|
+
TouchableOpacity: 'onPress={() => {}}',
|
|
119
|
+
Pressable: 'onPress={() => {}}',
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function generateTestFile({
|
|
123
|
+
componentName,
|
|
124
|
+
pascalName,
|
|
125
|
+
kebabName,
|
|
126
|
+
importPath,
|
|
127
|
+
category,
|
|
128
|
+
hasChildren,
|
|
129
|
+
childrenText,
|
|
130
|
+
requiredProps,
|
|
131
|
+
}) {
|
|
132
|
+
const testId = `perf-test-${kebabName}`;
|
|
133
|
+
const instanceName = `${pascalName.charAt(0).toLowerCase() + pascalName.slice(1)}PerfTest`;
|
|
134
|
+
|
|
135
|
+
const allProps = [`testID={this.testId}`];
|
|
136
|
+
if (requiredProps) {
|
|
137
|
+
// Parse individual JSX props, handling nested braces like {() => {}}
|
|
138
|
+
allProps.push(...splitProps(requiredProps));
|
|
139
|
+
}
|
|
140
|
+
allProps.push('style={styles.default}', '{...props}');
|
|
141
|
+
|
|
142
|
+
let createComponentBody;
|
|
143
|
+
if (hasChildren) {
|
|
144
|
+
const propLines = allProps.map(p => ` ${p}`).join('\n');
|
|
145
|
+
createComponentBody = ` return (\n <${componentName}\n${propLines}>\n ${childrenText}\n </${componentName}>\n );`;
|
|
146
|
+
} else if (allProps.length > 3) {
|
|
147
|
+
const propLines = allProps.map(p => ` ${p}`).join('\n');
|
|
148
|
+
createComponentBody = ` return (\n <${componentName}\n${propLines}\n />\n );`;
|
|
149
|
+
} else {
|
|
150
|
+
createComponentBody = ` return <${componentName} ${allProps.join(' ')} />;`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `/**
|
|
154
|
+
* Copyright (c) Microsoft Corporation.
|
|
155
|
+
* Licensed under the MIT License.
|
|
156
|
+
*
|
|
157
|
+
* Performance tests for ${componentName} component.
|
|
158
|
+
*
|
|
159
|
+
* @format
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
import * as React from 'react';
|
|
163
|
+
import {${componentName}, StyleSheet} from '${importPath}';
|
|
164
|
+
import {
|
|
165
|
+
ComponentPerfTestBase,
|
|
166
|
+
measurePerf,
|
|
167
|
+
} from '@react-native-windows/perf-testing';
|
|
168
|
+
import type {IScenario, PerfMetrics} from '@react-native-windows/perf-testing';
|
|
169
|
+
|
|
170
|
+
class ${pascalName}PerfTest extends ComponentPerfTestBase {
|
|
171
|
+
readonly componentName = '${componentName}';
|
|
172
|
+
readonly category = '${category}' as const;
|
|
173
|
+
readonly testId = '${testId}';
|
|
174
|
+
|
|
175
|
+
createComponent(props?: Record<string, unknown>): React.ReactElement {
|
|
176
|
+
${createComponentBody}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
getCustomScenarios(): IScenario[] {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const ${instanceName} = new ${pascalName}PerfTest();
|
|
185
|
+
|
|
186
|
+
describe('${componentName} Performance', () => {
|
|
187
|
+
test('mount time', async () => {
|
|
188
|
+
const perf = await ${instanceName}.measureMount();
|
|
189
|
+
expect(perf).toMatchPerfSnapshot();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('unmount time', async () => {
|
|
193
|
+
const perf = await ${instanceName}.measureUnmount();
|
|
194
|
+
expect(perf).toMatchPerfSnapshot();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('rerender time', async () => {
|
|
198
|
+
const perf = await ${instanceName}.measureRerender();
|
|
199
|
+
expect(perf).toMatchPerfSnapshot();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const styles = StyleSheet.create({
|
|
204
|
+
default: {
|
|
205
|
+
});
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function main() {
|
|
210
|
+
const rl = readline.createInterface({
|
|
211
|
+
input: process.stdin,
|
|
212
|
+
output: process.stdout,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
console.log('\n๐งช React Native Windows โ Performance Test Generator\n');
|
|
216
|
+
console.log('This will create a new .perf-test.tsx file with mount,');
|
|
217
|
+
console.log('unmount, and rerender tests for your component.\n');
|
|
218
|
+
console.log('Examples:');
|
|
219
|
+
console.log(
|
|
220
|
+
' Component: ScrollView | Import: react-native | Category: core | Children: yes',
|
|
221
|
+
);
|
|
222
|
+
console.log(
|
|
223
|
+
' Component: Button | Import: react-native | Category: core | Children: no',
|
|
224
|
+
);
|
|
225
|
+
console.log(
|
|
226
|
+
' Component: Image | Import: react-native | Category: core | Children: no',
|
|
227
|
+
);
|
|
228
|
+
console.log(
|
|
229
|
+
' Component: Pressable | Import: react-native | Category: interactive | Children: yes',
|
|
230
|
+
);
|
|
231
|
+
console.log(
|
|
232
|
+
' Component: Slider | Import: @react-native-community/slider | Category: community | Children: no',
|
|
233
|
+
);
|
|
234
|
+
console.log('');
|
|
235
|
+
|
|
236
|
+
const rawName = await ask(
|
|
237
|
+
rl,
|
|
238
|
+
'Component name (e.g. ScrollView, Image, Pressable)',
|
|
239
|
+
);
|
|
240
|
+
if (!rawName) {
|
|
241
|
+
console.error('โ Component name is required.');
|
|
242
|
+
rl.close();
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const pascalName = toPascalCase(rawName);
|
|
247
|
+
const kebabName = toKebabCase(rawName);
|
|
248
|
+
|
|
249
|
+
const importPath = await ask(
|
|
250
|
+
rl,
|
|
251
|
+
'Import path for the component',
|
|
252
|
+
'react-native',
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const category = await choose(
|
|
256
|
+
rl,
|
|
257
|
+
'Select component category:',
|
|
258
|
+
CATEGORIES,
|
|
259
|
+
0,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const hasChildrenAnswer = await ask(
|
|
263
|
+
rl,
|
|
264
|
+
'Does this component render children? (y/N)',
|
|
265
|
+
'N',
|
|
266
|
+
);
|
|
267
|
+
const hasChildren = hasChildrenAnswer.toLowerCase() === 'y';
|
|
268
|
+
let childrenText = 'Sample Content';
|
|
269
|
+
if (hasChildren) {
|
|
270
|
+
const customChildren = await ask(
|
|
271
|
+
rl,
|
|
272
|
+
'Children text/content',
|
|
273
|
+
'Sample Content',
|
|
274
|
+
);
|
|
275
|
+
childrenText = customChildren;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let requiredProps = KNOWN_REQUIRED_PROPS[rawName] || '';
|
|
279
|
+
if (requiredProps) {
|
|
280
|
+
console.log(`\n Auto-detected required props: ${requiredProps}`);
|
|
281
|
+
const override = await ask(
|
|
282
|
+
rl,
|
|
283
|
+
'Required props (Enter to accept, or type custom)',
|
|
284
|
+
requiredProps,
|
|
285
|
+
);
|
|
286
|
+
requiredProps = override;
|
|
287
|
+
} else {
|
|
288
|
+
const customProps = await ask(
|
|
289
|
+
rl,
|
|
290
|
+
'Required props (e.g. title="Test" onPress={() => {}}) โ leave blank if none',
|
|
291
|
+
'',
|
|
292
|
+
);
|
|
293
|
+
requiredProps = customProps;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
rl.close();
|
|
297
|
+
|
|
298
|
+
const categoryDir = path.join(PERF_TEST_ROOT, category);
|
|
299
|
+
if (!fs.existsSync(categoryDir)) {
|
|
300
|
+
fs.mkdirSync(categoryDir, {recursive: true});
|
|
301
|
+
console.log(
|
|
302
|
+
`\n๐ Created directory: ${path.relative(process.cwd(), categoryDir)}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const outputFile = path.join(categoryDir, `${pascalName}.perf-test.tsx`);
|
|
307
|
+
if (fs.existsSync(outputFile)) {
|
|
308
|
+
console.error(
|
|
309
|
+
`\nโ File already exists: ${path.relative(process.cwd(), outputFile)}`,
|
|
310
|
+
);
|
|
311
|
+
console.error(' Delete it first or choose a different name.');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const content = generateTestFile({
|
|
316
|
+
componentName: rawName,
|
|
317
|
+
pascalName,
|
|
318
|
+
kebabName,
|
|
319
|
+
importPath,
|
|
320
|
+
category,
|
|
321
|
+
hasChildren,
|
|
322
|
+
childrenText,
|
|
323
|
+
requiredProps,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
fs.writeFileSync(outputFile, content, 'utf8');
|
|
327
|
+
|
|
328
|
+
const relPath = path.relative(process.cwd(), outputFile);
|
|
329
|
+
console.log(`\nโ
Created: ${relPath}`);
|
|
330
|
+
console.log(`\nNext steps:`);
|
|
331
|
+
console.log(` 1. Review and customize the generated test file`);
|
|
332
|
+
console.log(` 2. Add any component-specific custom scenarios`);
|
|
333
|
+
console.log(` 3. Run: yarn perf --testPathPattern=${pascalName}`);
|
|
334
|
+
console.log(
|
|
335
|
+
` 4. Generate baseline: yarn perf:update --testPathPattern=${pascalName}`,
|
|
336
|
+
);
|
|
337
|
+
console.log('');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
main().catch(err => {
|
|
341
|
+
console.error('Error:', err);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
});
|