Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/macos-paper-storybook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@fluentui-react-native/storybook-desktop': minor
'@fluentui-react-native/components': patch
---

Add a macOS-only `--paper` option to prepare, build, launch, and smoke test Storybook with Paper while preserving Fabric as the default renderer.

Use centered alignment in macOS Paper CompoundItemLayout stories to avoid the native Yoga baseline crash without changing the primitive or Fabric demonstrations.

Measure LayoutStableText's hidden reserve Text directly in native alignment stories, avoiding Paper Text-versus-View pixel-rounding mismatches without relaxing the geometry assertions.

Deduplicate compatible Storybook React, theming, and shared UI dependencies in the lockfile after desktop UI updates, keeping strict Metro duplicate checks enabled.
51 changes: 51 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,57 @@ jobs:
apps/storybook/artifacts/storybook-commands
${{ steps.desktop-driver.outputs.doctor-path }}

macos-paper-storybook:
name: macOS Paper Storybook PR
runs-on: macos-26
timeout-minutes: 40
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up toolchain
uses: microsoft/react-native-test-app/.github/actions/setup-toolchain@c75754b0fedce160a4dcf7193fae1a6ece76efe2 # 5.4.11
with:
platform: macos
node-version: 22

- name: Install dependencies
run: yarn

- name: Build packages
run: yarn build

- name: Set up macOS Desktop Driver
id: desktop-driver
uses: ./.github/actions/setup-desktop-driver
with:
platform: macos
disabled-input-features: physicalClick

- name: Bundle macOS Paper
run: yarn storybook bundle --macos --paper
working-directory: apps/storybook

- name: Pod install (Paper)
run: yarn storybook prep --macos --paper
working-directory: apps/storybook

- name: Run through Paper storybook smoke tests
run: yarn storybook smoke --macos --paper --mode stories-and-tests
working-directory: apps/storybook
env:
CCACHE_DISABLE: 1

- name: Upload macOS Paper Storybook artifacts
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Storybook_macos_Paper_Dump
path: |
apps/storybook/artifacts/macos
apps/storybook/artifacts/storybook-commands
${{ steps.desktop-driver.outputs.doctor-path }}

ios:
name: iOS PR
runs-on: macos-26
Expand Down
2 changes: 2 additions & 0 deletions apps/storybook/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ Read [`agent-map.yaml`](agent-map.yaml) first for the compact architecture, look
repository root because subprocess dependency resolution must start in this workspace.
- Run `yarn storybook bundle --macos` for the JavaScript bundle, `yarn storybook build --macos` for a non-launching
native build, and `yarn storybook smoke --macos` for the complete owned lifecycle.
- macOS defaults to Fabric. Add `--paper` to `prep`, `build`, `run`, or `smoke` for Paper; regenerate with `prep`
whenever switching renderers because both modes share the generated project and Pods. Keep the endpoint `macos`.
- Preserve the shared smoke instance context: its canonical-root hash coordinates the macOS bundle identifier,
Storybook port, Metro port, generated runtime polyfill, and exact app shutdown. Do not replace those values with
process-name matching or fixed smoke ports.
Expand Down
28 changes: 28 additions & 0 deletions apps/storybook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,29 @@ yarn storybook run --macos

Requires Xcode + CocoaPods.

Fabric is the default macOS renderer. Add `--paper` to use Paper with the same
macOS story catalog and native Desktop Driver:

```sh
yarn storybook prep --macos --paper
yarn storybook build --macos --paper
yarn storybook smoke --macos --paper --mode stories-and-tests
```

For interactive development, use `yarn storybook driver --macos --paper` and
`yarn storybook run --macos --paper` in separate terminals. `--paper` is supported
only on macOS; it sets `RCT_NEW_ARCH_ENABLED=0` for child commands. Omitting it
selects Fabric explicitly, even if the parent environment disables the New Architecture.
Run `prep` again whenever switching renderers: the generated Xcode project and
Pods are shared, so Paper and Fabric cannot run concurrently from one enlistment.
To return to Fabric, run `yarn storybook prep --macos` before building or launching.
The **macOS Paper Storybook PR** job runs the full catalog traversal and authored
tests separately from the existing Fabric job.
CompoundItemLayout's demos use centered alignment in macOS Paper because the
current native text-baseline callback can return NaN and terminate Yoga
([upstream issue](https://github.com/microsoft/react-native-macos/issues/3121)).
Fabric and other platforms retain the baseline demonstration. The primitive itself is unchanged.

Run `yarn storybook smoke --macos` for the complete server, Metro, build/launch, all-story traversal, and
ownership-safe shutdown lifecycle. The shared CLI hashes this enlistment's canonical project root,
uses that suffix in the native bundle identifier, and selects dedicated Storybook and Metro ports.
Expand Down Expand Up @@ -165,6 +188,11 @@ them. Win32 continues to exercise Callout through its Paper endpoint.
Storybook's development bundle intentionally contains separate `pretty-format` and `react-is`
versions used by its internal tooling. They are excluded from the duplicate-module enforcement;
React, React Native, and application dependencies remain checked.
After upgrading Storybook UI or addon packages, run
`yarn dedupe '@storybook/*' storybook --strategy highest` from the repository
root to align compatible locked React, theming, and shared UI dependencies.
Keep these runtime packages covered by Metro's duplicate checks rather than
adding exclusions for mismatched versions.

## Running on Win32

Expand Down
5 changes: 5 additions & 0 deletions apps/storybook/agent-map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ native:
disabled_input_env: FURN_DESKTOP_DRIVER_DISABLED_INPUT_FEATURES
ci_policy: explicit-build-then-prebuilt-only
macos:
renderer:
default: fabric
paper_flag: --paper
switching: rerun-prep-before-build-or-launch
concurrency: one-renderer-per-enlistment
process: ReactTestApp
app_bundle: macos/DerivedData/Build/Products/Debug/ReactTestApp.app
coordinate_notes:
Expand Down
2 changes: 1 addition & 1 deletion apps/storybook/macos/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ end
# react-native-test-app runs codegen before autolinking, so seed its clean-install input first.
write_storybook_autolinking_config!(File.expand_path('..', __dir__))

use_test_app!(:fabric_enabled => true, :bridgeless_enabled => false) do |target|
use_test_app!(:fabric_enabled => ENV.fetch('RCT_NEW_ARCH_ENABLED', '1') != '0', :bridgeless_enabled => false) do |target|
target.app do
platform :osx, '14'
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** @jsxImportSource @fluentui-react-native/framework-base */
import type { ReactNode } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Platform, StyleSheet, Text, View } from 'react-native';

import type { Meta, StoryObj } from '@storybook/react-native';

Expand Down Expand Up @@ -55,7 +55,8 @@ const styles = StyleSheet.create({
width: 360,
},
textBaseline: {
alignItems: 'baseline',
// macOS Paper baseline crash: https://github.com/microsoft/react-native-macos/issues/3121
alignItems: Platform.OS === 'macos' && Reflect.get(globalThis, 'nativeFabricUIManager') == null ? 'center' : 'baseline',
},
leading: {
alignItems: 'center',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @jsxImportSource @fluentui-react-native/framework-base */
import { StyleSheet, Text } from 'react-native';
import { Platform, StyleSheet, Text } from 'react-native';

import { render } from '../../common/renderWithTheme';

Expand Down Expand Up @@ -42,4 +42,37 @@ describe('CompoundItemLayout', () => {
await component.rerender(<CompoundItemLayout primary={<Text>Primary</Text>} testID="text-row" />);
expect(StyleSheet.flatten(component.getByTestId('text-row').props.style)).toMatchObject({ alignItems: 'center' });
});

it.each([
['macos', false, 'center'],
['macos', true, 'baseline'],
['windows', true, 'baseline'],
['win32', false, 'baseline'],
])('uses %s story alignment with Fabric=%s', (platform, fabric, alignment) => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'nativeFabricUIManager');
const create = jest.fn(StyleSheet.create);
Object.defineProperty(globalThis, 'nativeFabricUIManager', { configurable: true, value: fabric ? {} : undefined });

try {
jest.isolateModules(() => {
jest.doMock('react-native', () =>
Object.create(jest.requireActual('react-native'), {
Platform: { value: { ...Platform, OS: platform } },
StyleSheet: { value: { ...StyleSheet, create } },
}),
);
require('./compound-item-layout.stories');
});
expect(create.mock.calls.map(([styles]) => styles)).toEqual(
expect.arrayContaining([expect.objectContaining({ textBaseline: { alignItems: alignment } })]),
);
} finally {
jest.dontMock('react-native');
if (descriptor) {
Object.defineProperty(globalThis, 'nativeFabricUIManager', descriptor);
} else {
Reflect.deleteProperty(globalThis, 'nativeFabricUIManager');
}
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,13 @@ function AlignmentMeasurement() {
const [visible, setVisible] = useState<LayoutRectangle>();
return (
<View>
<View
accessible
role="group"
style={styles.stableText}
testID="layout-stable-text-large-reserve"
onLayout={(event) => setReserve(event.nativeEvent.layout)}
>
<View accessible role="group" style={styles.stableText} testID="layout-stable-text-large-reserve">
<LayoutStableText
reserve={<Text style={styles.large}>Stable label</Text>}
reserve={
<Text style={styles.large} onLayout={(event) => setReserve(event.nativeEvent.layout)}>
Stable label
</Text>
}
visible={
<Text
accessible
Expand Down Expand Up @@ -164,7 +162,7 @@ export const Overview: Story = {
'centers the intrinsic smaller line inside its larger reserve': async ({ browser, expect, platform }) => {
const assert: typeof import('node:assert') = (await import('node:assert')).default;
if (platform === 'macos') {
// Fabric paragraphs do not expose their testID to AX; use their native onLayout measurements.
// Measure both Text line boxes; Paper rounds Text and wrapper View frames differently.
const status = await browser.$('~layout-stable-text-native-metrics');
await browser.waitUntil(async () => (await status.getText()).startsWith('{'));
const metrics: { reserve: LayoutRectangle; visible: LayoutRectangle } = JSON.parse(await status.getText());
Expand All @@ -178,7 +176,7 @@ export const Overview: Story = {
);
assert(
Math.abs(metrics.visible.x) <= 1 && metrics.visible.width <= metrics.reserve.width + 1,
'Visible text must retain leading alignment and fit the reserved width.',
`Visible text must retain leading alignment and fit the reserved width: ${JSON.stringify(metrics)}.`,
);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ describe('LayoutStableText', () => {
expect(StyleSheet.flatten(component.getByTestId('stable-text').props.style)).toMatchObject({ width: 80 });
});

it('preserves native layout measurements for both the hidden reserve and visible text', async () => {
const onReserveLayout = jest.fn();
const onVisibleLayout = jest.fn();
const component = await render(
<LayoutStableText
reserve={<Text onLayout={onReserveLayout}>Reserve</Text>}
visible={
<Text onLayout={onVisibleLayout} testID="visible">
Visible
</Text>
}
/>,
);
const reserveLayout = { nativeEvent: { layout: { x: 0, y: 0, width: 103, height: 24 } } };
const visibleLayout = { nativeEvent: { layout: { x: 0, y: 4, width: 104, height: 16 } } };

await fireEvent(component.getByText('Reserve', { includeHiddenElements: true }), 'layout', reserveLayout);
await fireEvent(component.getByTestId('visible'), 'layout', visibleLayout);

expect(onReserveLayout).toHaveBeenCalledWith(reserveLayout);
expect(onVisibleLayout).toHaveBeenCalledWith(visibleLayout);
});

it('preserves visible text semantics and interaction through the overlay', async () => {
const onPress = jest.fn();
const component = await render(
Expand Down
8 changes: 8 additions & 0 deletions packages/agentic/storybook-desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ storybook-desktop smoke --windows --mode stories-and-tests
```

Use `--config <path>` for a differently named configuration file.
macOS defaults to Fabric. Add `--paper` to macOS commands to set
`RCT_NEW_ARCH_ENABLED=0` for their child processes; commands without the flag
explicitly select Fabric. The endpoint, story catalog, and native driver remain
`macos`. Consuming Podfiles must honor `RCT_NEW_ARCH_ENABLED` when passing
`fabric_enabled` to `use_test_app!`. Run `prep` with the desired flag whenever
switching renderers because the generated native project and Pods are shared.
The matching API option is `new DesktopStorybookCli(config, { paper: true })`.

`build-driver` builds only the source-shipped native helper. `prep` first
ensures that helper, then installs CocoaPods on macOS or generates the React
Native Test App solution on Windows; Win32 prep now ensures the shared Windows
Expand Down
Loading
Loading