Skip to content

feat: experimental playfn #751

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: next
Choose a base branch
from
31 changes: 16 additions & 15 deletions examples/expo-example/.rnstorybook/index.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
// import { LiteUI } from '@storybook/react-native-ui-lite';
import { SafeAreaView, StatusBar, View } from 'react-native';
// import { SafeAreaView, StatusBar, View } from 'react-native';

import { view } from './storybook.requires';

const isScreenshotTesting = process.env.EXPO_PUBLIC_SCREENSHOT_TESTING === 'true';
// const isScreenshotTesting = process.env.EXPO_PUBLIC_SCREENSHOT_TESTING === 'true';

const StorybookUIRoot = view.getStorybookUI({
shouldPersistSelection: true,
storage: {
getItem: AsyncStorage.getItem,
setItem: AsyncStorage.setItem,
},
enableWebsockets: false,
enableWebsockets: true,
// onDeviceUI: !isScreenshotTesting,
host: 'localhost',
port: 7007,

// CustomUIComponent: LiteUI,
CustomUIComponent: isScreenshotTesting
? ({ children, story }) => {
return (
<SafeAreaView style={{ flex: 1 }}>
<StatusBar hidden />
<View style={{ flex: 1 }} accessibilityLabel={story?.id} testID={story?.id} accessible>
{children}
</View>
</SafeAreaView>
);
}
: undefined,
// CustomUIComponent: isScreenshotTesting
// ? ({ children, story }) => {
// return (
// <SafeAreaView style={{ flex: 1 }}>
// <StatusBar hidden />
// <View style={{ flex: 1 }} accessibilityLabel={story?.id} testID={story?.id} accessible>
// {children}
// </View>
// </SafeAreaView>
// );
// }
// : undefined,
});

export default StorybookUIRoot;
3 changes: 2 additions & 1 deletion examples/expo-example/.rnstorybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ const main: StorybookConfig = {
'storybook-addon-deep-controls',
'./local-addon-example',
],

reactNative: {
playFn: false,
playFn: true,
},

framework: '@storybook/react-native',
Expand Down
2 changes: 1 addition & 1 deletion examples/expo-example/.rnstorybook/storybook.requires.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ global.STORIES = normalizedStories;
module?.hot?.accept?.();

const options = {
"playFn": false
"playFn": true
}

if (!global.view) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import { NativeScreen } from '@storybook/react-native/NativeEvents';
import { expect, fn } from 'storybook/test';
import { ActionButton } from './Actions';
import { fn } from 'storybook/test';

const meta = {
component: ActionButton,
Expand Down Expand Up @@ -28,6 +29,16 @@ type Story = StoryObj<typeof meta>;
export const Basic: Story = {
args: {
text: 'Press me!',
onPress: fn(),
onPress: fn((e) => {
e.persist();
}),
},
play: async ({ args }) => {
const screen = new NativeScreen();

const button = await screen.getByText('Press me!');
await button.tap(0.5);

expect(args.onPress).toHaveBeenCalled();
},
};
6 changes: 3 additions & 3 deletions examples/expo-example/components/ActionExample/Actions.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
import { TouchableOpacity, Text, StyleSheet, TouchableOpacityProps } from 'react-native';

export interface ActionButtonProps {
onPress?: () => void;
onPress?: TouchableOpacityProps['onPress'];
text: string;
}

export const ActionButton = ({ onPress, text }: ActionButtonProps) => {
return (
<TouchableOpacity style={styles.container} onPress={onPress}>
<TouchableOpacity accessibilityRole="button" style={styles.container} onPress={onPress}>
<Text style={styles.text}>{text}</Text>
</TouchableOpacity>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import { fn } from 'storybook/test';
import { NativeScreen } from '@storybook/react-native/NativeEvents';
import { expect, fn } from 'storybook/test';
import { LoginForm } from './LoginForm';

const meta = {
Expand Down Expand Up @@ -47,3 +48,37 @@ export const LongErrors: Story = {
'Your password must be at least 8 characters long and contain both letters and numbers for security.',
},
};

export const InteractiveLogin: Story = {
args: {
onSubmit: fn(),
},
play: async ({ args }) => {
const screen = new NativeScreen();

await new Promise((resolve) => setTimeout(resolve, 1000));

// Find and tap the email input field by placeholder
const emailInput = await screen.getByPlaceholder('Enter your email');
await emailInput.tap();

// Type email address
await emailInput.type('[email protected]');

// Find and tap the password input field by placeholder
const passwordInput = await screen.getByPlaceholder('Enter your password');
await passwordInput.tap();

// Type password
await passwordInput.type('securePassword123');

// Find and tap the sign in button by text
const signInButton = await screen.getByText('Sign In');
await signInButton.tap();

await new Promise((resolve) => setTimeout(resolve, 1000));

// Verify that onSubmit was called with the correct arguments
expect(args.onSubmit).toHaveBeenCalledWith('[email protected]', 'securePassword123');
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const TextInput: React.FC<TextInputProps> = ({
value={value}
onChangeText={onChangeText}
secureTextEntry={secureTextEntry}
autoCapitalize="none"
/>
{error && <Text style={styles.error}>{error}</Text>}
</View>
Expand Down
10 changes: 9 additions & 1 deletion examples/expo-example/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ defaultConfig.resolver.nodeModulesPaths = [

const withStorybook = require('@storybook/react-native/metro/withStorybook');

module.exports = withStorybook(defaultConfig);
const storybookOptions = {
websockets: {
port: 7007,
host: 'localhost',
deviceId: '0868A689-5B78-4F52-A63A-60CAA848BB88',
},
};

module.exports = withStorybook(defaultConfig, storybookOptions);

/* , {
enabled: process.env.STORYBOOK_ENABLED === 'true',
Expand Down
2 changes: 1 addition & 1 deletion examples/expo-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"android": "EXPO_PUBLIC_STORYBOOK_ENABLED=true expo start --android",
"ios": "EXPO_PUBLIC_STORYBOOK_ENABLED=true expo start --ios",
"web": "EXPO_PUBLIC_STORYBOOK_ENABLED=true expo start --web",
"storybook": "EXPO_PUBLIC_STORYBOOK_ENABLED=true expo start -c",
"storybook": "EXPO_PUBLIC_STORYBOOK_ENABLED=true expo start",
"storybook:test": "EXPO_PUBLIC_SCREENSHOT_TESTING=true EXPO_PUBLIC_STORYBOOK_ENABLED=true expo start -c",
"storybook:web": "storybook dev -p 6006",
"build-web-storybook": "storybook build",
Expand Down
13 changes: 8 additions & 5 deletions packages/react-native-ui-lite/src/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
ViewStyle,
} from 'react-native';
import { SET_CURRENT_STORY } from 'storybook/internal/core-events';
import type { Selection } from '@storybook/react-native-ui-common';
import { addons } from 'storybook/internal/manager-api';
import { type API_IndexHash } from 'storybook/internal/types';
import { AddonsTabs, MobileAddonsPanel, MobileAddonsPanelRef } from './MobileAddonsPanel';
Expand Down Expand Up @@ -166,10 +167,12 @@ export const Layout = ({
const mobileMenuDrawerRef = useRef<MobileMenuDrawerRef>(null);
const addonPanelRef = useRef<MobileAddonsPanelRef>(null);

const setSelection = useCallback(({ storyId: newStoryId }: { storyId: string }) => {
const channel = addons.getChannel();
const setSelection = useCallback((selection: Selection) => {
if (selection) {
const channel = addons.getChannel();

channel.emit(SET_CURRENT_STORY, { storyId: newStoryId });
channel.emit(SET_CURRENT_STORY, { storyId: selection.storyId });
}
}, []);

return (
Expand Down Expand Up @@ -239,7 +242,7 @@ export const Layout = ({
testID="mobile-menu-button"
style={navButtonStyle}
hitSlop={navButtonHitSlop}
onPress={() => mobileMenuDrawerRef.current?.setMobileMenuOpen(true)}
onPress={() => mobileMenuDrawerRef.current?.setMobileMenuOpen?.(true)}
>
<MenuIcon color={theme.color.mediumdark} />
<Text style={navButtonTextStyle} numberOfLines={1}>
Expand All @@ -249,7 +252,7 @@ export const Layout = ({

<IconButton
testID="mobile-addons-button"
onPress={() => addonPanelRef.current.setAddonsPanelOpen(true)}
onPress={() => addonPanelRef.current?.setAddonsPanelOpen?.(true)}
Icon={BottomBarToggleIcon}
/>
</Nav>
Expand Down
23 changes: 22 additions & 1 deletion packages/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,28 @@
"./metro/withStorybook": "./dist/metro/withStorybook.js",
"./preview": "./dist/preview.js",
"./scripts/generate": "./scripts/generate.js",
"./preset": "./preset.js"
"./preset": "./preset.js",
"./webserver": "./dist/webserver/webserver.js",
"./NativeEvents": "./dist/webserver/NativeEvents.js"
},
"typesVersions": {
"*": {
"*": [
"./dist/index.d.ts"
],
"webserver": [
"./dist/webserver/webserver.d.ts"
],
"metro/withStorybook": [
"./dist/metro/withStorybook.d.ts"
],
"preview": [
"./dist/preview.d.ts"
],
"NativeEvents": [
"./dist/webserver/NativeEvents.d.ts"
]
}
},
"files": [
"bin/**/*",
Expand Down
27 changes: 9 additions & 18 deletions packages/react-native/src/metro/withStorybook.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as path from 'path';
import { generate } from '../../scripts/generate';
import { WebSocketServer, WebSocket, Data } from 'ws';

import type { MetroConfig } from 'metro-config';
import { setupWebsocketServer } from '../webserver/webserver';

/**
* Options for configuring WebSockets used for syncing storybook instances or sending events to storybook.
Expand All @@ -16,6 +17,11 @@ interface WebsocketsOptions {
* The host WebSocket server will bind to. Defaults to 'localhost'.
*/
host?: string;

/**
* The device ID to use for test events over the WebSocket server.
*/
deviceId?: string;
}

/**
Expand Down Expand Up @@ -127,24 +133,9 @@ function withStorybook(
if (websockets) {
const port = websockets.port ?? 7007;
const host = websockets.host ?? 'localhost';
const deviceId = websockets.deviceId;

const wss = new WebSocketServer({ port, host });

wss.on('connection', function connection(ws: WebSocket) {
console.log('WebSocket connection established');

ws.on('error', console.error);

ws.on('message', function message(data: Data) {
try {
const json = JSON.parse(data.toString());

wss.clients.forEach((wsClient) => wsClient.send(JSON.stringify(json)));
} catch (error) {
console.error(error);
}
});
});
setupWebsocketServer({ port, host, deviceId });
}

generate({
Expand Down
Loading