Files
b51e189da0 chore(mobile): Add mobile Storybook screenshots to PRs (#6868)
* fix(ci): Fix web Storybook screenshots and add mobile support

## Web Storybook Screenshots Fixes

- Fix story ID generation to use `title` from meta export instead of file paths
  - Previously generated wrong IDs like `features-portfolio-components-x`
  - Now correctly generates `features-portfolio-x` matching Storybook's IDs
- Fix git credentials for pushing to storage branch
  - Was failing with "could not read Username" error
  - Now uses same working credential helper as page-screenshots workflow

## Mobile Storybook Screenshots (New)

Add new workflow to capture and post mobile component screenshots to PRs:
- Triggers on changes to `apps/mobile/src/**/*.stories.tsx`
- Builds mobile Storybook for web using React Native Web
- Serves locally and captures with Playwright (iPhone 14 Pro viewport)
- Posts screenshots to PR with 📱 Mobile Storybook Screenshots heading

New files:
- .github/workflows/mobile-storybook-screenshots.yml
- .github/scripts/generate-mobile-story-urls.js
- .github/scripts/capture-mobile-screenshots.js

* test: Add test changes to trigger screenshot workflows

- Add title to web CopyButton story (was missing explicit title)
- Add layout parameter to mobile SafeButton story

These changes test the screenshot workflows on this PR.

* fix(ci): Add path traversal protection to mobile screenshot server

Prevent potential path traversal attacks by:
- Resolving and normalizing the static directory path
- Validating that resolved file paths stay within the static directory
- Returning 403 Forbidden for any path traversal attempts

* fix(ci): Fix mobile Storybook build and add security fixes

- Run build from apps/mobile directory to fix monorepo path resolution
- Add --disable-telemetry flag to avoid telemetry module resolution error
- Add path traversal protection to static file server

* fix(ci): Add graceful handling for mobile Storybook build failures

The mobile Storybook build has a known issue with monorepo path
resolution for telemetry metadata. This update:

- Makes the build step continue on error with a warning
- Skips screenshot capture when build fails
- Prevents workflow failure when Storybook build issues occur

* fix(ci): Address Copilot review suggestions

- Fix regex pattern for title extraction to handle quotes properly
  (handles titles with apostrophes like "It's working")
- Fix binary content encoding in static server (don't use utf-8 for images/fonts)
- Filter out error screenshots from PR comments
- Use greedy regex matching for component names with hyphens

* chore: Remove test story changes

Revert test changes made to trigger screenshot workflows:
- Remove title from CopyButton story
- Remove layout parameter from SafeButton story

* refactor: Remove web storybook fixes (moved to separate PR)

The web storybook screenshot fixes have been extracted to PR #6870.
This PR now only contains the new mobile storybook screenshots feature.

* chore(mobile): Fix theme type errors and trigger mobile screenshots workflow

- Add optional chaining to theme property access throughout mobile app
- Fixes TypeScript errors where theme properties were possibly undefined
- Add comment to SafeButton story to trigger mobile screenshots workflow

* fix(mobile): Disable Storybook telemetry to fix monorepo build

Add core.disableTelemetry config to prevent telemetry module from
attempting to resolve framework packages in monorepo structure,
which was causing the build to fail.

* fix(mobile): Use standard framework config instead of path resolution

Replace path resolution workaround with standard framework configuration
to fix 'Cannot find module' error in CI environment.

* fix(ci): Access Storybook iframe for mobile screenshots

Storybook renders stories inside an iframe, so we need to:
1. Wait for the iframe to load
2. Access the iframe's content frame
3. Look for #storybook-root inside the iframe

This fixes the 'element is hidden' error in screenshot capture.

* fix(ci): Add comprehensive mocks for mobile Storybook web build

Fix mobile Storybook rendering by mocking React Native modules that
don't work in web browsers.

Changes:
- Mock react-native-worklets with version 0.5.0 and all required functions
- Create standalone mock for react-native-reanimated (hooks, animations, utilities)
- Mock react-refresh to prevent production bundle errors
- Mock react-native-quick-crypto using Node.js crypto
- Update capture script to wait for #storybook-root to be visible
- Use webpack aliases to replace native modules with mocks

The mobile Storybook now successfully renders components in a web browser
and screenshots can be captured with Playwright.

* test: Trigger mobile screenshots workflow

Trigger the mobile screenshots workflow by modifying a story file.

* fix(ci): Set STORYBOOK_WEB=true for mobile Storybook build

The build needs STORYBOOK_WEB=true to use the web configuration with mocks.
Without this, it tries to build the native Storybook which doesn't have
the webpack aliases for React Native module mocks.

* fix(ci): Handle direct iframe.html navigation in screenshot capture

The script was looking for iframe#storybook-preview-iframe, but story URLs
navigate directly to iframe.html which renders content without the wrapper.

Changes:
- Detect if URL contains 'iframe.html' and use page directly
- Only look for iframe wrapper when on main Storybook page
- Simplify content selection to work with direct iframe rendering

Tested locally - all 5 screenshots now capture successfully.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2025-12-22 18:02:31 +01:00

131 lines
3.2 KiB
JavaScript

/* eslint-disable */
// Standalone mock for react-native-reanimated in Storybook
// Based on the official mock but self-contained
const NOOP = () => {};
const NOOP_FACTORY = () => NOOP;
const ID = (t) => t;
const IMMEDIATE_CALLBACK_INVOCATION = (callback) => callback();
// Mock hooks
const hook = {
useAnimatedProps: IMMEDIATE_CALLBACK_INVOCATION,
useEvent: () => NOOP,
useSharedValue: (init) => {
const value = { value: init };
return new Proxy(value, {
get(target, prop) {
if (prop === 'value') return target.value;
if (prop === 'get') return () => target.value;
if (prop === 'set') {
return (newValue) => {
if (typeof newValue === 'function') {
target.value = newValue(target.value);
} else {
target.value = newValue;
}
};
}
},
set(target, prop, newValue) {
if (prop === 'value') {
target.value = newValue;
return true;
}
return false;
},
});
},
useAnimatedStyle: IMMEDIATE_CALLBACK_INVOCATION,
useAnimatedReaction: NOOP,
useAnimatedRef: () => ({ current: null }),
useAnimatedScrollHandler: NOOP_FACTORY,
useDerivedValue: (processor) => {
const result = processor();
return { value: result, get: () => result };
},
useAnimatedSensor: () => ({
sensor: { value: { x: 0, y: 0, z: 0 } },
unregister: NOOP,
isAvailable: false,
config: { interval: 0 },
}),
useAnimatedKeyboard: () => ({ height: 0, state: 0 }),
useScrollViewOffset: () => ({ value: 0 }),
useScrollOffset: () => ({ value: 0 }),
};
// Mock animation functions
const animation = {
cancelAnimation: NOOP,
withDecay: (_config, callback) => {
callback?.(true);
return 0;
},
withDelay: (_delayMs, nextAnimation) => nextAnimation,
withRepeat: ID,
withSequence: () => 0,
withSpring: (toValue, _config, callback) => {
callback?.(true);
return toValue;
},
withTiming: (toValue, _config, callback) => {
callback?.(true);
return toValue;
},
};
// Mock utilities
const utilities = {
runOnUI: (fn) => (...args) => fn(...args),
runOnJS: (fn) => (...args) => fn(...args),
interpolate: (value) => value,
interpolateColor: (value) => value,
Easing: {
linear: ID,
ease: ID,
quad: ID,
cubic: ID,
},
Extrapolate: {
EXTEND: 'extend',
CLAMP: 'clamp',
IDENTITY: 'identity',
},
};
// Animated components (passthrough to regular React Native components)
const { Animated, View, Text, ScrollView, Image } = require('react-native');
const AnimatedView = View;
const AnimatedText = Text;
const AnimatedScrollView = ScrollView;
const AnimatedImage = Image;
// Create animated component wrapper
const createAnimatedComponent = (Component) => Component;
// Export everything
module.exports = {
...hook,
...animation,
...utilities,
createAnimatedComponent,
default: {
View: AnimatedView,
Text: AnimatedText,
ScrollView: AnimatedScrollView,
Image: AnimatedImage,
createAnimatedComponent,
...hook,
...animation,
...utilities,
},
// Animated components
View: AnimatedView,
Text: AnimatedText,
ScrollView: AnimatedScrollView,
Image: AnimatedImage,
__esModule: true,
};