From 8c8bd3c41b5a0bf54c20488430f8ebe1a4eda92d Mon Sep 17 00:00:00 2001 From: Dawid Krajewski Date: Sun, 25 Aug 2024 13:44:12 +0200 Subject: [PATCH] feat(react-native-storybook): isRunningVisualTests & handled errors (#40) --- examples/expo-example/package.json | 1 + .../src/components/screens/Test/Test.tsx | 18 ++++++++-- .../src/helpers/SherloModule.ts | 34 ++++++++++++++----- .../src/helpers/index.ts | 1 - .../src/helpers/isExpoGo.ts | 11 ------ packages/react-native-storybook/src/index.ts | 1 + .../src/isRunningVisualTests.ts | 7 ++++ .../src/openStorybook.ts | 13 ++++--- .../src/registerStorybook.ts | 21 ++++++++---- .../src/utils/handleAsyncError.ts | 11 ++++++ .../react-native-storybook/src/utils/index.ts | 1 + 11 files changed, 83 insertions(+), 36 deletions(-) delete mode 100644 packages/react-native-storybook/src/helpers/isExpoGo.ts create mode 100644 packages/react-native-storybook/src/isRunningVisualTests.ts create mode 100644 packages/react-native-storybook/src/utils/handleAsyncError.ts diff --git a/examples/expo-example/package.json b/examples/expo-example/package.json index 9dab9c76..defcf9bc 100644 --- a/examples/expo-example/package.json +++ b/examples/expo-example/package.json @@ -21,6 +21,7 @@ "sherlo:demo": "sherlo --config ../../configs/demo.preview.json", "sherlo:eas": "sherlo --config ../../configs/eas.preview.json", "sherlo:sync": "sherlo --config ../../configs/sync.preview.json", + "start": "expo start", "start:android:dev": "adb install builds/development/android.apk && expo start --android --dev-client", "start:android:go": "expo start --android --go", "start:ios:dev": "tar -xzvf builds/development/ios.tar.gz -C builds/ && xcrun simctl install booted builds/sherloexpoexample.app && expo start --ios --dev-client", diff --git a/examples/expo-example/src/components/screens/Test/Test.tsx b/examples/expo-example/src/components/screens/Test/Test.tsx index dabe449c..63b6e033 100644 --- a/examples/expo-example/src/components/screens/Test/Test.tsx +++ b/examples/expo-example/src/components/screens/Test/Test.tsx @@ -1,8 +1,7 @@ -import React from 'react'; -import { Text, View, StyleSheet } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; +import { isRunningVisualTests } from '@sherlo/react-native-storybook'; import * as Localization from 'expo-localization'; -import { useColorScheme } from 'react-native'; +import { Text, View, StyleSheet, useColorScheme } from 'react-native'; const Test = () => { const theme = useColorScheme(); // 'light' or 'dark' @@ -17,16 +16,29 @@ const Test = () => { + Language: {language} + + Country: {country} + + Theme: {theme || 'undefined'} + + + + + + isRunningVisualTests: {isRunningVisualTests.toString()} + + ); }; diff --git a/packages/react-native-storybook/src/helpers/SherloModule.ts b/packages/react-native-storybook/src/helpers/SherloModule.ts index bdfeb218..5b1fa712 100644 --- a/packages/react-native-storybook/src/helpers/SherloModule.ts +++ b/packages/react-native-storybook/src/helpers/SherloModule.ts @@ -1,7 +1,12 @@ import base64 from 'base-64'; import { NativeModules } from 'react-native'; import utf8 from 'utf8'; -import isExpoGo from './isExpoGo'; + +let isExpoGo = false; +try { + const Constants = require('expo-constants').default; + isExpoGo = Constants.appOwnership === 'expo'; +} catch {} type SherloModule = { getInitialMode: () => 'testing' | 'default'; @@ -20,12 +25,6 @@ if (SherloNativeModule !== null) { SherloModule = createSherloModule(); } else { SherloModule = createDummySherloModule(); - - if (!isExpoGo) { - console.warn( - '@sherlo/react-native-storybook: Sherlo native module is not accessible. Rebuild the app to link it on the native side.' - ); - } } export default SherloModule; @@ -65,13 +64,30 @@ function normalizePath(path: string): string { } function createDummySherloModule(): SherloModule { + const noNativeModuleErrorMessage = getNoNativeModuleErrorMessage(); + return { storybookRegistered: async () => {}, getInitialMode: () => 'default', appendFile: async () => {}, mkdir: async () => {}, readFile: async () => '', - openStorybook: async () => {}, - toggleStorybook: async () => {}, + openStorybook: async () => { + throw new Error(noNativeModuleErrorMessage); + }, + toggleStorybook: async () => { + throw new Error(noNativeModuleErrorMessage); + }, }; } + +function getNoNativeModuleErrorMessage() { + if (isExpoGo) { + return [ + '@sherlo/react-native-storybook: Accessing Storybook via Expo Go is not supported. Use a developer build or set up Storybook as a standalone app.', + 'Learn more: https://docs.sherlo.io/getting-started/setup#storybook-entry-point', + ].join('\n\n'); + } else { + return '@sherlo/react-native-storybook: Sherlo native module is not accessible. Rebuild the app to link it on the native side.'; + } +} diff --git a/packages/react-native-storybook/src/helpers/index.ts b/packages/react-native-storybook/src/helpers/index.ts index 3d386d0b..694f5ca4 100644 --- a/packages/react-native-storybook/src/helpers/index.ts +++ b/packages/react-native-storybook/src/helpers/index.ts @@ -1,3 +1,2 @@ -export { default as isExpoGo } from './isExpoGo'; export { default as RunnerBridge } from './RunnerBridge'; export { default as SherloModule } from './SherloModule'; diff --git a/packages/react-native-storybook/src/helpers/isExpoGo.ts b/packages/react-native-storybook/src/helpers/isExpoGo.ts deleted file mode 100644 index 09b39d7c..00000000 --- a/packages/react-native-storybook/src/helpers/isExpoGo.ts +++ /dev/null @@ -1,11 +0,0 @@ -let isExpoGo: boolean; - -try { - const Constants = require('expo-constants').default; - - isExpoGo = Constants.appOwnership === 'expo'; -} catch { - isExpoGo = false; -} - -export default isExpoGo; diff --git a/packages/react-native-storybook/src/index.ts b/packages/react-native-storybook/src/index.ts index 024f9acb..4f48cf5d 100644 --- a/packages/react-native-storybook/src/index.ts +++ b/packages/react-native-storybook/src/index.ts @@ -1,3 +1,4 @@ export { default as getStorybook } from './getStorybook'; +export { default as isRunningVisualTests } from './isRunningVisualTests'; export { default as openStorybook } from './openStorybook'; export { default as registerStorybook } from './registerStorybook'; diff --git a/packages/react-native-storybook/src/isRunningVisualTests.ts b/packages/react-native-storybook/src/isRunningVisualTests.ts new file mode 100644 index 00000000..163fca3a --- /dev/null +++ b/packages/react-native-storybook/src/isRunningVisualTests.ts @@ -0,0 +1,7 @@ +import { NativeModules } from 'react-native'; + +const { SherloModule } = NativeModules; + +const isRunningVisualTests = SherloModule?.getConstants().initialMode === 'testing'; + +export default isRunningVisualTests; diff --git a/packages/react-native-storybook/src/openStorybook.ts b/packages/react-native-storybook/src/openStorybook.ts index a9ef1f68..1063cb05 100644 --- a/packages/react-native-storybook/src/openStorybook.ts +++ b/packages/react-native-storybook/src/openStorybook.ts @@ -1,13 +1,16 @@ import { SherloModule } from './helpers'; +import { handleAsyncError } from './utils'; -function openStorybook(): Promise { - return SherloModule.openStorybook().catch((error) => { +function openStorybook(): void { + SherloModule.openStorybook().catch((error) => { if (error.code === 'NOT_REGISTERED') { - console.log( - 'To use `openStorybook()`, you need to first call `registerStorybook()`.\n\nLearn more: https://docs.sherlo.io/getting-started/setup?storybook-entry-point=integrated#storybook-entry-point' + handleAsyncError( + new Error( + 'To use `openStorybook()`, you need to first call `registerStorybook()`.\n\nLearn more: https://docs.sherlo.io/getting-started/setup?storybook-entry-point=integrated#storybook-entry-point' + ) ); } else { - throw error; + handleAsyncError(error); } }); } diff --git a/packages/react-native-storybook/src/registerStorybook.ts b/packages/react-native-storybook/src/registerStorybook.ts index b43924cc..8504c2d3 100644 --- a/packages/react-native-storybook/src/registerStorybook.ts +++ b/packages/react-native-storybook/src/registerStorybook.ts @@ -1,6 +1,12 @@ import { ReactElement } from 'react'; import { AppRegistry, DevSettings } from 'react-native'; import { SherloModule } from './helpers'; +import { handleAsyncError } from './utils'; + +let ExpoDevMenu: any; +try { + ExpoDevMenu = require('expo-dev-menu'); +} catch {} function registerStorybook(StorybookComponent: () => ReactElement) { AppRegistry.registerComponent('SherloStorybook', () => StorybookComponent); @@ -13,21 +19,22 @@ export default registerStorybook; /* ========================================================================== */ -let ExpoDevMenu: any; -try { - ExpoDevMenu = require('expo-dev-menu'); -} catch {} +let hasAddedDevMenuItem = false; function addToggleStorybookToDevMenu() { - // Only add the menu item in development builds - if (!__DEV__) return; + // Add menu item once in development build + if (!__DEV__ || hasAddedDevMenuItem) return; const MENU_LABEL = 'Toggle Storybook'; - const toggleStorybook = () => SherloModule.toggleStorybook(); + const toggleStorybook = () => { + SherloModule.toggleStorybook().catch(handleAsyncError); + }; DevSettings.addMenuItem(MENU_LABEL, toggleStorybook); if (ExpoDevMenu) { ExpoDevMenu.registerDevMenuItems([{ name: MENU_LABEL, callback: toggleStorybook }]); } + + hasAddedDevMenuItem = true; } diff --git a/packages/react-native-storybook/src/utils/handleAsyncError.ts b/packages/react-native-storybook/src/utils/handleAsyncError.ts new file mode 100644 index 00000000..b51db9c4 --- /dev/null +++ b/packages/react-native-storybook/src/utils/handleAsyncError.ts @@ -0,0 +1,11 @@ +function handleAsyncError(error: Error) { + /** + * setTimeout with 0 delay is used because it forces the error to be thrown on + * the main thread, ensuring React Native properly handles and displays the error + */ + setTimeout(() => { + throw error; + }, 0); +} + +export default handleAsyncError; diff --git a/packages/react-native-storybook/src/utils/index.ts b/packages/react-native-storybook/src/utils/index.ts index 8462c3d3..5a28bc33 100644 --- a/packages/react-native-storybook/src/utils/index.ts +++ b/packages/react-native-storybook/src/utils/index.ts @@ -1,2 +1,3 @@ export { default as getGlobalStates } from './getGlobalStates'; +export { default as handleAsyncError } from './handleAsyncError'; export { default as isObject } from './isObject';