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';