Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Conversation

yannbf
Copy link
Member

@yannbf yannbf commented Sep 15, 2025

Closes #

What I did

This PR introduces an API in storybook/manager-api to open a file in the editor:
openInEditor(filePath, lineNumber?, columnNumber?).

This new API is used in a few places to open a story in the editor:

  • Storybook toolbar (in both docs and story) - presented in a new share button
  • Story Context Menu
  • Interactions panel

Such buttons will not be shown in production builds, as this is a dev only feature.

Context Menu

Upon clicking on the three dots in stories only

image

Interactions panel

Presented in local stories only. In composed Storybooks or published Storybooks the text will just be a normal text and not a link
image

New share button

This new button groups the actions of copying the story URL, opening in isolation mode and introduces a QR Code for opening the network address from your phone
image

This PR also fixes an issue where the shortcut font was broken in a few places:
image

How it works

There is a very nice package called launch-editor which comes out of the box with Vite, and once it is present, you can fetch a specific route to open a file in the editor, it will figure out your existing editors and open the most active one, like magic. For Webpack projects, we need to include the launch-editor-middleware, but it works the same way.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • manual testing
  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

  1. Run Storybook either through sandboxes or yarn storybook:ui
  2. Select a story
  3. Click on both open in editor buttons, like the gif above

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This pull request has been released as version 0.0.0-pr-32452-sha-d0653ada. Try it out in a new sandbox by running npx storybook@0.0.0-pr-32452-sha-d0653ada sandbox or in an existing project with npx storybook@0.0.0-pr-32452-sha-d0653ada upgrade.

More information
Published version 0.0.0-pr-32452-sha-d0653ada
Triggered by @yannbf
Repository storybookjs/storybook
Branch yann/open-in-editor-feature
Commit d0653ada
Datetime Wed Sep 17 15:48:54 UTC 2025 (1758124134)
Workflow run 17803236525

To request a new release of this pull request, mention the @storybookjs/core team.

core team members can create a new canary release here or locally with gh workflow run --repo storybookjs/storybook canary-release-pr.yml --field pr=32452

Greptile Summary

Updated On: 2025-09-15 08:36:29 UTC

This PR implements an "open in editor" feature that allows developers to quickly open story files directly in their code editor from the Storybook UI. The implementation introduces a new openInEditor(filePath, lineNumber?, columnNumber?) API that leverages the launch-editor ecosystem to automatically detect and open files in the user's preferred editor.

Core Implementation:

  • New API Function: The openInEditor function in code/core/src/manager-api/lib/open-in-editor.ts makes POST requests to a /__open-in-editor endpoint with file path, line, and column information
  • Manager API Integration: The function is exported through the manager-api's public interface and global exports system
  • Builder Support: Webpack5 builder now includes launch-editor-middleware dependency and middleware setup (Vite already has built-in support)

UI Integration Points:

  • Main Toolbar: Added openInEditorTool to the default toolbar tools array, appearing in both story and docs view modes
  • Component Testing: Added an "Open in editor" button to the component testing subnav toolbar
  • Both UI integrations include proper conditional rendering (development mode only, local stories only) and accessibility features

How It Works:
The feature uses the launch-editor package ecosystem which automatically detects available editors and opens the most recently used one. For Vite projects, this works out of the box. For Webpack projects, the PR adds launch-editor-middleware to provide the same functionality. The implementation follows Storybook's established patterns for addon tools and maintains consistency across different UI contexts.

The change integrates seamlessly with Storybook's existing architecture, using the Consumer pattern to access story metadata and following established conventions for toolbar tools and API exports.

Confidence score: 3/5

  • This PR introduces useful functionality but has several implementation concerns that need attention
  • Score reflects potential issues with error handling, TypeScript integration, and missing builder support validation
  • Pay close attention to the Webpack5 builder implementation and the openInEditor function's error handling

Summary by CodeRabbit

  • New Features

    • Share menu in the preview toolbar: copy story link, open in isolation, and QR code for mobile access.
    • Open in editor: toolbar button, preview/context-menu/panel integration, and a new "Open in editor" action.
  • Improvements

    • Copy story name shows "Copied!" feedback and exposes copy-story-link shortcut.
    • Tool layout updated (share + open-in-editor added); tooltips now close only on Escape.
    • Settings label updated; macOS shortcut handling improved.
  • Documentation

    • Updated addon toolkit shortcut example.
  • Chores

    • Dependency bumps and expanded tests (sharing, context menu, settings, clipboard).

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 files reviewed, 2 comments

Edit Code Review Bot Settings | Greptile

if (!(storyId && isLocal && importPath)) {
return null;
}
const file = importPath;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: redundant variable assignment - importPath can be used directly instead of assigning to file

Suggested change
const file = importPath;
return (
<IconButton
key="open-in-editor"
onClick={() => openInEditor(importPath)}
title="Open in editor"
aria-label="Open in editor"
>
<VSCodeIcon />
</IconButton>
);

}) => {
const buttonText = status === 'errored' ? 'Scroll to error' : 'Scroll to end';
const theme = useTheme();
const onOpenInEditor = async (file: string | undefined) => openInEditor(file);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The async function wrapper is unnecessary since openInEditor already returns a Promise and the result isn't awaited

Suggested change
const onOpenInEditor = async (file: string | undefined) => openInEditor(file);
const onOpenInEditor = (file: string | undefined) => openInEditor(file);

Copy link

nx-cloud bot commented Sep 15, 2025

View your CI Pipeline Execution ↗ for commit cc2ffba

Command Status Duration Result
nx run-many -t build --parallel=3 ✅ Succeeded 50s View ↗

☁️ Nx Cloud last updated this comment at 2025-09-18 11:17:12 UTC

@storybook-pr-benchmarking
Copy link

storybook-pr-benchmarking bot commented Sep 15, 2025

Package Benchmarks

Commit: cc2ffba, ran on 18 September 2025 at 11:05:45 UTC

The following packages have significant changes to their size or dependencies:

storybook

Before After Difference
Dependency count 43 43 0
Self size 30.04 MB 30.13 MB 🚨 +94 KB 🚨
Dependency size 17.30 MB 17.30 MB 0 B
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 187 187 0
Self size 886 KB 886 KB 0 B
Dependency size 79.64 MB 79.73 MB 🚨 +94 KB 🚨
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 169 169 0
Self size 35 KB 35 KB 0 B
Dependency size 76.06 MB 76.16 MB 🚨 +94 KB 🚨
Bundle Size Analyzer Link Link

create-storybook

Before After Difference
Dependency count 44 44 0
Self size 1.55 MB 1.55 MB 🚨 +30 B 🚨
Dependency size 47.34 MB 47.43 MB 🚨 +94 KB 🚨
Bundle Size Analyzer node node

@yannbf yannbf force-pushed the yann/open-in-editor-feature branch from 8a3af8f to 5ab5822 Compare September 15, 2025 09:51
… open-in-editor functionality to Subnav and Preview components, allowing users to open files directly in their code editor. Updated package dependencies and added related types for improved type safety.
@yannbf yannbf force-pushed the yann/open-in-editor-feature branch from 0933141 to 1853097 Compare September 15, 2025 15:56
@yannbf yannbf force-pushed the yann/open-in-editor-feature branch from bfb941e to b665876 Compare September 15, 2025 17:32
@yannbf yannbf changed the title WIP - Core: Add "open in editor" feature Core: Add "open in editor" feature Sep 16, 2025
@ghengeveld ghengeveld self-requested a review September 16, 2025 08:09
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
code/core/src/manager/components/preview/tools/share.tsx (2)

15-15: Consider migrating to qrcode.react for better TypeScript support.

The current import suggests you've already migrated to qrcode.react which provides better TypeScript support and maintenance than the previously problematic react-qr-code. This is the correct choice.


91-96: Extract copyStoryLink to avoid potential undefined access.

While the code has been updated to use optional chaining (shortcutKeys?.copyStoryLink), extracting it to a variable would be cleaner and ensure consistency.

Apply this diff for clarity:

 const api = useStorybookApi();
 const shortcutKeys = api.getShortcutKeys();
 const enableShortcuts = !!shortcutKeys;
 const [copied, setCopied] = useState(false);
-const copyStoryLink = shortcutKeys?.copyStoryLink;
+const copyLinkKeys = shortcutKeys?.copyStoryLink;

And update line 105:

-right: enableShortcuts ? <Shortcut keys={copyStoryLink} /> : null,
+right: enableShortcuts ? <Shortcut keys={copyLinkKeys} /> : null,

And line 141:

-}, [baseUrl, storyId, queryParams, copied, qrUrl, enableShortcuts, copyStoryLink]);
+}, [baseUrl, storyId, queryParams, copied, qrUrl, enableShortcuts, copyLinkKeys]);
code/core/src/component-testing/components/Subnav.tsx (2)

51-53: Collapse conditions into a single, intention‑revealing boolean.

Compute isEditorOpenable = Boolean(canOpenInEditor && importPath) and use it in the render. Keeps logic centralized and avoids accidental regressions.

Example (outside the diffed lines):

const isEditorOpenable = Boolean(canOpenInEditor && importPath);
...
{isEditorOpenable ? (/* clickable */) : (/* text */)}

Also applies to: 127-129


192-214: Guard importPath and use a semantic button; avoid asserting undefined.

Clickable branch should render only when a valid importPath exists; otherwise you risk firing a request with “undefined” and creating a dangling listener. Also, render a real button for a11y. Add a label fallback when storyFileName is absent.

-              {canOpenInEditor ? (
+              {canOpenInEditor && Boolean(importPath) ? (
                 <WithTooltip
                   trigger="hover"
                   hasChrome={false}
                   tooltip={<Note note="Open in editor" />}
                 >
-                  <StyledLocation
-                    aria-label="Open in editor"
-                    onClick={() => {
-                      openInEditor({
-                        file: importPath as string,
-                      });
-                    }}
-                  >
-                    {storyFileName}
-                  </StyledLocation>
+                  <StyledLocation
+                    as="button"
+                    type="button"
+                    aria-label="Open in editor"
+                    onClick={() => openInEditor({ file: importPath! })}
+                  >
+                    {storyFileName ?? importPath!.split(/[\\/]/).pop()}
+                  </StyledLocation>
                 </WithTooltip>
               ) : (
-                <StyledLocation isText={true}>{storyFileName}</StyledLocation>
+                <StyledLocation isText>
+                  {storyFileName ?? importPath?.split(/[\\/]/).pop()}
+                </StyledLocation>
               )}
code/core/src/manager-api/lib/open-in-editor.ts (1)

14-25: Prevent dangling listeners: add timeout/cleanup and guard empty file.

If no response arrives (e.g., no middleware in prod/composed), the promise never resolves and the listener leaks. Also bail out early on empty file.

 export async function openInEditor(payload: {
   file: string;
   line?: number;
   column?: number;
 }): Promise<OpenInEditorResponsePayload> {
-  return new Promise((resolve) => {
-    const channel = addons.getChannel();
-    const { file, line, column } = payload;
-    const handler = (res: OpenInEditorResponsePayload) => {
-      if (res.file === file && res.line === line && res.column === column) {
-        channel.off(OPEN_IN_EDITOR_RESPONSE, handler);
-        resolve(res);
-      }
-    };
-    channel.on(OPEN_IN_EDITOR_RESPONSE, handler);
-    channel.emit(OPEN_IN_EDITOR_REQUEST, payload);
-  });
+  return new Promise((resolve) => {
+    const channel = addons.getChannel();
+    const { file, line, column } = payload;
+    if (!file?.trim()) {
+      resolve({ file, line, column, error: 'Missing file path' });
+      return;
+    }
+    let timer: ReturnType<typeof setTimeout>;
+    const handler = (res: OpenInEditorResponsePayload) => {
+      if (res.file === file && res.line === line && res.column === column) {
+        channel.off(OPEN_IN_EDITOR_RESPONSE, handler);
+        clearTimeout(timer);
+        resolve(res);
+      }
+    };
+    channel.on(OPEN_IN_EDITOR_RESPONSE, handler);
+    channel.emit(OPEN_IN_EDITOR_REQUEST, payload);
+    timer = setTimeout(() => {
+      channel.off(OPEN_IN_EDITOR_RESPONSE, handler);
+      resolve({ file, line, column, error: 'Timed out waiting for editor response' });
+    }, 10000);
+  });
 }
🧹 Nitpick comments (9)
code/core/src/manager-api/lib/stories.ts (1)

334-338: Add defensive check before accessing children array.

While the component node should always have children (established by prior logic), adding a safety check prevents potential runtime errors.

Apply this diff to add a safety check:

 if (item.type === 'component') {
   // attach importPath to the component node which should be the same for all children
   // this way we can add "open in editor" to the component node
-  item.importPath = acc[item.children[0]].importPath;
+  if (item.children?.length > 0 && acc[item.children[0]]?.importPath) {
+    item.importPath = acc[item.children[0]].importPath;
+  }
 }
code/core/src/manager/components/preview/tools/share.tsx (1)

155-157: Consider URL construction robustness.

The logic assumes STORYBOOK_NETWORK_ADDRESS is a complete URL with protocol. Consider validating or normalizing it to handle edge cases where the address might be provided without a protocol.

Consider adding validation:

-const storyUrl = global.STORYBOOK_NETWORK_ADDRESS
-  ? `${global.STORYBOOK_NETWORK_ADDRESS}${window.location.search}`
-  : window.location.href;
+const networkAddress = global.STORYBOOK_NETWORK_ADDRESS;
+const storyUrl = networkAddress
+  ? networkAddress.startsWith('http') 
+    ? `${networkAddress}${window.location.search}`
+    : `http://${networkAddress}${window.location.search}`
+  : window.location.href;
code/e2e-tests/manager.spec.ts (1)

48-49: Update selector to match renamed label.

The test still references the old "Shortcuts" label but should use "Settings" to match the UI change.

Apply this diff:

// toggle with menu item
-await sbPage.page.locator('[aria-label="Shortcuts"]').click();
+await sbPage.page.locator('[aria-label="Settings"]').click();
await sbPage.page.locator('#list-item-S').click();

Also applies to: 132-133, 171-171, 222-222, 251-251

code/core/src/component-testing/components/Subnav.tsx (1)

79-83: Reset native button chrome for StyledLocation.

When rendered “as=button”, ensure no default button styling leaks through.

 const StyledLocation = styled(P)<{ isText?: boolean }>(({ theme, isText }) => ({
   color: isText ? theme.textMutedColor : theme.color.secondary,
   cursor: isText ? 'default' : 'pointer',
   fontWeight: isText ? theme.typography.weight.regular : theme.typography.weight.bold,
+  background: 'transparent',
+  border: 0,
+  padding: 0,
   justifyContent: 'flex-end',
   textAlign: 'right',
   whiteSpace: 'nowrap',
   marginTop: 'auto',
   marginBottom: 1,
   paddingRight: 15,
   fontSize: 13,
 }));
code/core/src/manager-api/lib/open-in-editor.ts (1)

15-15: Optional: avoid getChannel() to improve determinism/testability.

Consider accepting an optional channel/api in args, defaulting to addons.getChannel(), so callers can inject and tests can stub.

code/core/src/manager/components/sidebar/Menu.tsx (4)

84-93: Mobile label is inconsistent; use “Settings” for parity

The mobile button still says “About Storybook” while the desktop one is “Settings”. Align for consistency and a11y.

Apply:

-          title="About Storybook"
-          aria-label="About Storybook"
+          title="Settings"
+          aria-label="Settings"

108-126: Add aria-expanded/aria-haspopup for a11y

Expose tooltip/menu state to assistive tech.

Apply:

       <SidebarIconButton
         title="Settings"
         aria-label="Settings"
+        aria-haspopup="menu"
+        aria-expanded={isTooltipVisible}
         highlighted={!!isHighlighted}
         active={isTooltipVisible}
         size="medium"
         isMobile={false}
       >

17-22: Avoid leaking styling-only props to the DOM

highlighted/isMobile may get forwarded to the DOM via IconButton. Use transient props and filter.

Apply:

-export const SidebarIconButton = styled(IconButton)<
-  ComponentProps<typeof Button> & {
-    highlighted: boolean;
-    isMobile: boolean;
-  }
->(({ highlighted, theme, isMobile }) => ({
+export const SidebarIconButton = styled(IconButton, {
+  // emotion option; supported by storybook/theming’s styled
+  shouldForwardProp: (prop) => prop !== '$highlighted' && prop !== '$isMobile',
+})<
+  ComponentProps<typeof Button> & {
+    $highlighted: boolean;
+    $isMobile: boolean;
+  }
+>(({ $highlighted: highlighted, theme, $isMobile: isMobile }) => ({

Also update usages in this file to pass $highlighted/$isMobile instead of highlighted/isMobile.


70-74: Remove ts-expect-error by aligning onClick types

Use the Button/IconButton onClick type to satisfy TS and drop the suppression.

Apply:

 export interface SidebarMenuProps {
   menu: MenuList;
   isHighlighted?: boolean;
-  onClick?: React.MouseEventHandler<HTMLButtonElement>;
+  onClick?: ComponentProps<typeof Button>['onClick'];
 }

And remove the suppression:

-          // @ts-expect-error (non strict)
           onClick={onClick}

Also applies to: 88-90

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0653ad and bbfa9e9.

📒 Files selected for processing (12)
  • code/core/package.json (3 hunks)
  • code/core/src/component-testing/components/Subnav.stories.tsx (2 hunks)
  • code/core/src/component-testing/components/Subnav.tsx (5 hunks)
  • code/core/src/core-server/server-channel/open-in-editor-channel.ts (1 hunks)
  • code/core/src/manager-api/lib/open-in-editor.ts (1 hunks)
  • code/core/src/manager-api/lib/stories.ts (1 hunks)
  • code/core/src/manager-api/modules/shortcuts.ts (4 hunks)
  • code/core/src/manager/components/preview/tools/open-in-editor.tsx (1 hunks)
  • code/core/src/manager/components/preview/tools/share.tsx (1 hunks)
  • code/core/src/manager/components/sidebar/ContextMenu.tsx (6 hunks)
  • code/core/src/manager/components/sidebar/Menu.tsx (1 hunks)
  • code/e2e-tests/manager.spec.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • code/core/src/core-server/server-channel/open-in-editor-channel.ts
  • code/core/src/manager-api/modules/shortcuts.ts
  • code/core/src/manager/components/preview/tools/open-in-editor.tsx
  • code/core/package.json
  • code/core/src/manager/components/sidebar/ContextMenu.tsx
🧰 Additional context used
📓 Path-based instructions (1)
code/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

code/**/*.{test,spec}.{ts,tsx}: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx

Files:

  • code/e2e-tests/manager.spec.ts
🧬 Code graph analysis (4)
code/e2e-tests/manager.spec.ts (1)
code/e2e-tests/util.ts (1)
  • SbPage (8-203)
code/core/src/component-testing/components/Subnav.tsx (1)
code/core/src/manager-api/lib/open-in-editor.ts (1)
  • openInEditor (9-26)
code/core/src/manager-api/lib/open-in-editor.ts (3)
code/core/src/manager-api/index.mock.ts (1)
  • openInEditor (5-5)
code/core/src/manager-api/index.ts (1)
  • openInEditor (3-3)
code/core/src/core-events/data/open-in-editor.ts (1)
  • OpenInEditorResponsePayload (3-8)
code/core/src/manager/components/preview/tools/share.tsx (2)
code/core/src/manager-api/root.tsx (2)
  • Combo (112-115)
  • useStorybookApi (292-295)
code/core/src/manager/container/Menu.tsx (1)
  • Shortcut (44-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (8)
code/core/src/component-testing/components/Subnav.stories.tsx (2)

1-1: LGTM!

The React import is necessary for JSX syntax in the story file.


138-151: LGTM! Story properly demonstrates the open-in-editor feature.

The new story correctly showcases the "Open in editor" functionality with appropriate args configuration.

code/e2e-tests/manager.spec.ts (3)

19-34: Comprehensive test coverage for Settings tooltip behavior.

The test thoroughly validates tooltip visibility, keyboard handling (especially Escape key), and click-outside behavior. Well structured test case.


57-96: Excellent coverage of context menu actions.

The test comprehensively validates "Open in editor" functionality across component, docs, and story nodes, plus clipboard verification for "Copy story name". The hover/click interactions are well sequenced.


98-109: Well-structured share actions test.

The test properly validates the share functionality, including visibility checks and clipboard content verification after copying the story link.

code/core/src/component-testing/components/Subnav.tsx (1)

22-22: LGTM: correct manager‑api import for openInEditor.

code/core/src/manager-api/lib/open-in-editor.ts (1)

1-26: All call sites use the {file: …} payload — guard potentially undefined file values.

No bare-string calls found. Callers pass expressions that may be undefined: fullAPI.getCurrentStoryData().importPath, context.importPath, importPath (including "importPath as string"). Audit/guard these call sites or noop if file is falsy:

  • code/core/src/manager/components/preview/tools/open-in-editor.tsx — openInEditor({ file: importPath })
  • code/core/src/manager-api/modules/shortcuts.ts — openInEditor({ file: fullAPI.getCurrentStoryData().importPath })
  • code/core/src/manager/components/sidebar/ContextMenu.tsx — openInEditor({ file: context.importPath })
  • code/core/src/component-testing/components/Subnav.tsx — openInEditor({ file: importPath as string })
code/core/src/manager/components/sidebar/Menu.tsx (1)

116-117: LGTM: “Settings” label

Desktop rename looks good and matches the new UX.

Run to ensure no lingering “Shortcuts” labels remain:

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (8)
code/e2e-tests/manager.spec.ts (1)

93-95: Consider adding assertion for exact clipboard content.

The test currently uses toContain('Primary') which could match partial strings. Consider using a more precise assertion to ensure the exact story name is copied.

-      await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
-        'Primary'
-      );
+      await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toBe('Primary');
code/core/src/manager-api/modules/open-in-editor.tsx (5)

16-18: Remove redundant SubState interface.

The SubState interface with notifications field appears to be unnecessary since this module doesn't manage notifications state - it only uses the notifications API to display errors. The notifications state is already managed by the notifications module.

-export interface SubState {
-  notifications: API_Notification[];
-}

42-46: Potential race condition with concurrent requests.

The current implementation matches responses based on file, line, and column. If multiple requests for the same location are made concurrently, only the first one will resolve while others may hang indefinitely since the handler is removed after the first match.

Consider using a unique request ID to properly match request-response pairs:

-        const handler = (res: OpenInEditorResponsePayload) => {
-          if (res.file === file && res.line === line && res.column === column) {
-            provider.channel?.off(OPEN_IN_EDITOR_RESPONSE, handler);
-            resolve(res);
-          }
+        const requestId = `${Date.now()}-${Math.random()}`;
+        const handler = (res: OpenInEditorResponsePayload & { requestId?: string }) => {
+          if (res.requestId === requestId || 
+              (res.file === file && res.line === line && res.column === column)) {
+            provider.channel?.off(OPEN_IN_EDITOR_RESPONSE, handler);
+            resolve(res);
+          }
         };
         provider.channel?.on(OPEN_IN_EDITOR_RESPONSE, handler);
-        provider.channel?.emit(OPEN_IN_EDITOR_REQUEST, payload);
+        provider.channel?.emit(OPEN_IN_EDITOR_REQUEST, { ...payload, requestId });

40-50: Add timeout handling for unresponsive requests.

The promise never rejects if no response is received, which could leave the UI in a pending state indefinitely.

Consider adding a timeout:

       return new Promise((resolve) => {
         const { file, line, column } = payload;
+        let timeoutId: NodeJS.Timeout;
         const handler = (res: OpenInEditorResponsePayload) => {
           if (res.file === file && res.line === line && res.column === column) {
+            clearTimeout(timeoutId);
             provider.channel?.off(OPEN_IN_EDITOR_RESPONSE, handler);
             resolve(res);
           }
         };
+        timeoutId = setTimeout(() => {
+          provider.channel?.off(OPEN_IN_EDITOR_RESPONSE, handler);
+          resolve({ file, line, column, error: 'Request timed out' });
+        }, 5000);
         provider.channel?.on(OPEN_IN_EDITOR_RESPONSE, handler);
         provider.channel?.emit(OPEN_IN_EDITOR_REQUEST, payload);
       });

54-54: Remove unused state declaration.

The state variable initializes notifications but this module doesn't manage notification state.

-  const state: SubState = { notifications: [] };

And update the return statement:

   return {
     api,
-    state,
+    state: {},
     init: async () => {

67-68: Improve error message fallback.

The fallback uses logical OR which could show "Check the Storybook process..." even when payload.error is an empty string.

-              subHeadline:
-                payload.error ||
-                'Check the Storybook process on the command line for more details.',
+              subHeadline: payload.error || undefined,
+              ...(payload.error ? {} : {
+                subHeadline: 'Check the Storybook process on the command line for more details.'
+              }),

Or more simply:

-              subHeadline:
-                payload.error ||
-                'Check the Storybook process on the command line for more details.',
+              subHeadline: payload.error ? 
+                payload.error : 
+                'Check the Storybook process on the command line for more details.',
code/core/src/manager-api/modules/shortcuts.ts (2)

393-398: Add null safety for getCurrentStoryData().

The code assumes getCurrentStoryData() returns a valid object with an importPath property, but it might return null or undefined.

         case 'openInEditor': {
           if (global.CONFIG_TYPE === 'DEVELOPMENT') {
-            fullAPI.openInEditor({
-              file: fullAPI.getCurrentStoryData().importPath,
-            });
+            const storyData = fullAPI.getCurrentStoryData();
+            if (storyData?.importPath) {
+              fullAPI.openInEditor({
+                file: storyData.importPath,
+              });
+            }
           }
           break;
         }

409-410: Consider adding user feedback for successful copy.

The copy operation happens silently. Consider adding a notification to confirm the link was copied to clipboard.

         case 'copyStoryLink': {
-          copy(window.location.href);
+          const success = copy(window.location.href);
+          if (success && fullAPI.addNotification) {
+            fullAPI.addNotification({
+              id: 'copy-story-link',
+              content: { 
+                headline: 'Story link copied to clipboard' 
+              },
+              duration: 2000,
+            });
+          }
           break;
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bbfa9e9 and 3b49d88.

⛔ Files ignored due to path filters (1)
  • code/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • code/core/src/component-testing/components/InteractionsPanel.tsx (4 hunks)
  • code/core/src/component-testing/components/Panel.tsx (3 hunks)
  • code/core/src/component-testing/components/Subnav.tsx (5 hunks)
  • code/core/src/core-server/server-channel/open-in-editor-channel.ts (1 hunks)
  • code/core/src/manager-api/modules/notifications.ts (1 hunks)
  • code/core/src/manager-api/modules/open-in-editor.tsx (1 hunks)
  • code/core/src/manager-api/modules/shortcuts.ts (4 hunks)
  • code/core/src/manager-api/root.tsx (2 hunks)
  • code/core/src/manager/components/preview/tools/open-in-editor.tsx (1 hunks)
  • code/core/src/manager/components/sidebar/ContextMenu.tsx (6 hunks)
  • code/core/src/manager/globals/exports.ts (2 hunks)
  • code/e2e-tests/manager.spec.ts (7 hunks)
✅ Files skipped from review due to trivial changes (1)
  • code/core/src/manager-api/modules/notifications.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • code/core/src/manager/globals/exports.ts
  • code/core/src/component-testing/components/Subnav.tsx
  • code/core/src/core-server/server-channel/open-in-editor-channel.ts
  • code/core/src/component-testing/components/InteractionsPanel.tsx
  • code/core/src/manager/components/preview/tools/open-in-editor.tsx
  • code/core/src/component-testing/components/Panel.tsx
  • code/core/src/manager/components/sidebar/ContextMenu.tsx
🧰 Additional context used
📓 Path-based instructions (1)
code/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

code/**/*.{test,spec}.{ts,tsx}: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx

Files:

  • code/e2e-tests/manager.spec.ts
🧬 Code graph analysis (3)
code/core/src/manager-api/root.tsx (1)
code/core/src/manager-api/modules/open-in-editor.tsx (1)
  • openInEditor (35-51)
code/core/src/manager-api/modules/open-in-editor.tsx (2)
code/core/src/manager-api/modules/notifications.ts (3)
  • SubState (7-9)
  • SubAPI (12-27)
  • init (29-62)
code/core/src/core-events/data/open-in-editor.ts (1)
  • OpenInEditorResponsePayload (3-8)
code/e2e-tests/manager.spec.ts (1)
code/e2e-tests/util.ts (1)
  • SbPage (8-203)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (7)
code/e2e-tests/manager.spec.ts (4)

19-34: LGTM! Well-structured test for Settings tooltip behavior.

The test covers the important scenarios: keyboard dismissal with Escape, persistence with other keys, and dismissal on outside clicks.


98-109: Good test coverage for share actions.

The test properly verifies both the UI presence and the clipboard functionality for the story link feature.


48-49: Consistent replacement of 'Shortcuts' with 'Settings'.

All references to the Shortcuts menu have been properly updated to use the Settings menu instead, maintaining test consistency with the UI changes.

Also applies to: 132-133, 135-136, 171-172, 222-223, 251-252


57-96: Verify the open-in-editor functionality works across different environments.

The test correctly verifies that "Open in editor" options are available in the UI for component, docs, and story nodes. However, since this is a development-only feature, ensure it's properly hidden in production builds.

code/core/src/manager-api/root.tsx (1)

56-56: LGTM! Clean integration of the openInEditor module.

The new module is properly imported and integrated into the modules array following the established pattern.

Also applies to: 180-180

code/core/src/manager-api/modules/shortcuts.ts (2)

115-118: LGTM! New shortcut type definitions.

The new shortcuts are properly added to the API_Shortcuts interface.


154-155: Good default shortcut choices.

The keyboard shortcuts follow platform conventions and don't conflict with existing shortcuts. Alt+Shift+E for editor and Alt+Shift+L for link are intuitive choices.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (8)
code/e2e-tests/manager.spec.ts (7)

8-8: Normalize STORYBOOK_TYPE and accept “development”

Make env check case‑insensitive and handle common alias.

-const isStorybookDev = (process.env.STORYBOOK_TYPE || 'dev') === 'dev';
+const isStorybookDev = ['dev', 'development'].includes(
+  (process.env.STORYBOOK_TYPE ?? 'dev').toLowerCase()
+);

20-35: Reduce flakiness when dismissing the tooltip

Clicking 'body' can hit overlays. Prefer a guaranteed page‑outside click.

-      await page.click('body');
+      await page.mouse.click(0, 0);

49-49: Prefer role+name over aria‑label CSS selector

More resilient to attribute drift and improves readability.

-      await sbPage.page.locator('[aria-label="Settings"]').click();
+      await sbPage.page.getByRole('button', { name: 'Settings' }).click();

136-136: Use role+name selector for Settings trigger

Keep selectors consistent and accessible.

-      await sbPage.page.locator('[aria-label="Settings"]').click();
+      await sbPage.page.getByRole('button', { name: 'Settings' }).click();
@@
-      await sbPage.page.locator('[aria-label="Settings"]').click();
+      await sbPage.page.getByRole('button', { name: 'Settings' }).click();

Also applies to: 139-139


175-175: Use role+name for Settings opener

Same rationale as above.

-        await sbPage.page.locator('[aria-label="Settings"]').click();
+        await sbPage.page.getByRole('button', { name: 'Settings' }).click();

225-225: Use role+name for Settings trigger

Consistency and robustness.

-      await sbPage.page.locator('[aria-label="Settings"]').click();
+      await sbPage.page.getByRole('button', { name: 'Settings' }).click();

255-255: Use role+name for Settings trigger

Same improvement here.

-      await sbPage.page.locator('[aria-label="Settings"]').click();
+      await sbPage.page.getByRole('button', { name: 'Settings' }).click();
code/core/src/manager-api/tests/stories.test.ts (1)

1539-1581: Optional: make assertions less snapshot‑fragile

To reduce churn, consider asserting only the critical bits (presence of importPath on 'a' and on its children) with toMatchObject, keeping the rest as-is.

Apply this focused assertion after the snapshot:

+      // Ensure editor integration metadata is present without over-specifying the structure
+      expect(filteredIndex!.a).toMatchObject({ type: 'component', importPath: './a.ts' });
+      expect(filteredIndex!['a--1']).toMatchObject({ type: 'story', importPath: './a.ts' });
+      expect(filteredIndex!['a--2']).toMatchObject({ type: 'story', importPath: './a.ts' });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b49d88 and af37fd7.

📒 Files selected for processing (3)
  • code/core/src/component-testing/components/Subnav.tsx (5 hunks)
  • code/core/src/manager-api/tests/stories.test.ts (3 hunks)
  • code/e2e-tests/manager.spec.ts (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/core/src/component-testing/components/Subnav.tsx
🧰 Additional context used
📓 Path-based instructions (2)
code/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

code/**/*.{test,spec}.{ts,tsx}: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx

Files:

  • code/core/src/manager-api/tests/stories.test.ts
  • code/e2e-tests/manager.spec.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/spy-mocking.mdc)

**/*.test.{ts,tsx,js,jsx}: Use vi.mock() with the spy: true option for all package and file mocks in Vitest tests
Place all mocks at the top of the test file before any test cases
Use vi.mocked() to type and access mocked functions
Implement mock behaviors in beforeEach blocks
Mock all required dependencies that the test subject uses
Mock implementations should be placed in beforeEach blocks
Each mock implementation should return a Promise for async functions
Mock implementations should match the expected return type of the original function
Use vi.mocked() to access and implement mock behaviors
Mock all required properties and methods that the test subject uses
Avoid direct function mocking without vi.mocked()
Avoid mock implementations outside of beforeEach blocks
Avoid mocking without the spy: true option
Avoid inline mock implementations within test cases
Avoid mocking only a subset of required dependencies
Mock at the highest level of abstraction needed
Keep mock implementations simple and focused
Use type-safe mocking with vi.mocked()
Document complex mock behaviors
Group related mocks together

Files:

  • code/core/src/manager-api/tests/stories.test.ts
🧬 Code graph analysis (1)
code/e2e-tests/manager.spec.ts (1)
code/e2e-tests/util.ts (1)
  • SbPage (8-203)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (1)
code/core/src/manager-api/tests/stories.test.ts (1)

1415-1456: LGTM — verify importPath exposure is consistent

Tests now assert importPath on component entries in filteredIndex. Confirm that the index types and the indexer → manager mapping always set importPath for component entries (and never add importPath to root/group) to avoid snapshot churn.

Quick grep to run locally:

rg -nP --type=ts -C2 '\b(interface|type)\s+.*(IndexEntry|.*Entry)\b' code
rg -nP --type=ts -C3 "type:\\s*'component'.*importPath" code
rg -nP --type=ts -C3 "type:\\s*'(root|group)'.*importPath" code

Comment on lines +1496 to +1523
{
"a": {
"children": [
"a--1",
],
"depth": 0,
"id": "a",
"importPath": "./a.ts",
"name": "a",
"parent": undefined,
"renderLabel": undefined,
"tags": [],
"type": "component",
},
"a--1": {
"depth": 1,
"id": "a--1",
"importPath": "./a.ts",
"name": "1",
"parent": "a",
"prepared": false,
"renderLabel": undefined,
"tags": [],
"title": "a",
"type": "story",
},
}
`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Minor: define deterministic source for component.importPath when children differ

Snapshot looks good, but if a component’s stories span multiple files, a single component.importPath can be ambiguous and nondeterministic across invalidations. Prefer a stable rule (e.g., first child by stable sort, or prefer the current/selected story’s importPath in UI).

If the UI’s open-in-editor uses component.importPath as a fallback, confirm it prefers story.importPath when available:


🏁 Script executed:

#!/bin/bash
# Find open-in-editor callsites and how importPath is chosen
rg -nP -C3 "openInEditor\\(|importPath\\b" code

Length of output: 299943


Minor: make component.importPath deterministic or prefer story.importPath for Open in editor

Verified: component.importPath is populated from the first child (core/src/manager-api/lib/stories.ts:335–338) — nondeterministic when a component's stories live in multiple files. Sidebar ContextMenu uses context.importPath for "Open in editor" (core/src/manager/components/sidebar/ContextMenu.tsx:51–61), while preview/shortcuts call Open in editor with the story's importPath (core/src/manager/components/preview/tools/open-in-editor.tsx:21–45; core/src/manager-api/modules/shortcuts.ts:393–396). Action: either set component.importPath using a deterministic rule (stable sort or file-order) or change the UI to prefer the selected/story.importPath when opening the editor.

🤖 Prompt for AI Agents
In code/core/src/manager-api/tests/stories.test.ts around lines 1496–1523 the
test fixtures expose that component.importPath is derived nondeterministically
from the first child, which can cause "Open in editor" to open the wrong file;
fix by changing the UI logic (where ContextMenu/preview/shortcuts call Open in
editor) to prefer the story's importPath when present and only fall back to
component.importPath, or alternatively make component.importPath deterministic
(stable sort or file-order) when building the component object—implement the
former by updating the ContextMenu and any Open-in-editor call sites to use
selectedStory.importPath || component.importPath.

Comment on lines 58 to 101
test('Story context menu actions', async ({ page }) => {
test.skip(!isStorybookDev, 'These actions are only applicable in dev mode');
const sbPage = new SbPage(page, expect);
await sbPage.navigateToStory('example/button', 'docs');

// Context menu should contain open in editor for component node
await page.locator('[data-item-id="example-button"]').hover();
await page
.locator('[data-item-id="example-button"] div[data-testid="context-menu"] button')
.click();
const sidebarContextMenu = page.getByTestId('tooltip');
await expect(
sidebarContextMenu.getByRole('button', { name: /open in editor/i })
).toBeVisible();
await page.click('body');

// Context menu should contain open in editor for docs node
await page.locator('[data-item-id="example-button--docs"]').hover();
await page
.locator('[data-item-id="example-button--docs"] div[data-testid="context-menu"] button')
.click();
await expect(
page.getByTestId('tooltip').getByRole('button', { name: /open in editor/i })
).toBeVisible();
await page.click('body');

// Context menu should contain open in editor and copy story name for story node
await page.locator('[data-item-id="example-button--primary"]').hover();
await page
.locator('[data-item-id="example-button--primary"] div[data-testid="context-menu"] button')
.click();
await expect(
page.getByTestId('tooltip').getByRole('button', { name: /open in editor/i })
).toBeVisible();
await page
.getByTestId('tooltip')
.getByRole('button', { name: /copy story name/i })
.click();

await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
'Primary'
);
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Clipboard assertions may fail without permissions; grant and poll

Reading from navigator.clipboard often needs clipboard permissions and a brief delay. Grant at test setup and use expect.poll for stability.

-      await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
-        'Primary'
-      );
+      await expect
+        .poll(() => page.evaluate(() => navigator.clipboard.readText()))
+        .toContain('Primary');

Add this once in test.beforeEach (outside this hunk):

// before page.goto(storybookUrl)
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], { origin: storybookUrl });

I can submit a follow‑up patch wiring this into the suite if you’d like.

🤖 Prompt for AI Agents
In code/e2e-tests/manager.spec.ts around lines 58 to 101, the clipboard
assertion reads navigator.clipboard directly which can fail without clipboard
permissions and timing; grant clipboard-read/clipboard-write for the Storybook
origin once in a test.beforeEach (before navigating to the story) using
page.context().grantPermissions([...], { origin: storybookUrl }) and replace the
direct evaluate read+expect with a stable poll-based assertion (expect.poll or a
short retry loop) that reads navigator.clipboard.readText() until it contains
'Primary' to avoid flaky failures.

Comment on lines 102 to 114
test('Story share actions', async ({ page }) => {
const sbPage = new SbPage(page, expect);
await sbPage.navigateToStory('example/button', 'primary');
await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await expect(page.getByRole('button', { name: /copy story link/i })).toBeVisible();
await page.getByRole('button', { name: /copy story link/i }).click();

await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
`${storybookUrl}/?path=/story/example-button--primary`
);
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Gate “Open in editor” visibility by dev mode; fix clipboard read flake

Open‑in‑editor is dev‑only per PR; assert conditionally. Also switch to polling clipboard.

-      await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible();
+      if (isStorybookDev) {
+        await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible();
+      } else {
+        await expect(page.getByRole('button', { name: 'Open in editor' })).toHaveCount(0);
+      }
@@
-      await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
-        `${storybookUrl}/?path=/story/example-button--primary`
-      );
+      await expect
+        .poll(() => page.evaluate(() => navigator.clipboard.readText()))
+        .toContain(`${storybookUrl}/?path=/story/example-button--primary`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('Story share actions', async ({ page }) => {
const sbPage = new SbPage(page, expect);
await sbPage.navigateToStory('example/button', 'primary');
await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await expect(page.getByRole('button', { name: /copy story link/i })).toBeVisible();
await page.getByRole('button', { name: /copy story link/i }).click();
await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
`${storybookUrl}/?path=/story/example-button--primary`
);
});
test('Story share actions', async ({ page }) => {
const sbPage = new SbPage(page, expect);
await sbPage.navigateToStory('example/button', 'primary');
if (isStorybookDev) {
await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible();
} else {
await expect(page.getByRole('button', { name: 'Open in editor' })).toHaveCount(0);
}
await page.getByRole('button', { name: 'Share' }).click();
await expect(page.getByRole('button', { name: /copy story link/i })).toBeVisible();
await page.getByRole('button', { name: /copy story link/i }).click();
await expect
.poll(() => page.evaluate(() => navigator.clipboard.readText()))
.toContain(`${storybookUrl}/?path=/story/example-button--primary`);
});
🤖 Prompt for AI Agents
In code/e2e-tests/manager.spec.ts around lines 102 to 114, the test
unconditionally asserts the "Open in editor" button (which is present only in
dev mode) and reads the clipboard directly (causing flakiness); change the test
to first check whether the suite is running in dev mode and only assert the
"Open in editor" visibility when dev mode is true, and replace the single
navigator.clipboard.readText() call with a polling/retry-based clipboard read
(e.g., loop or waitFor/waitForFunction that repeatedly reads
navigator.clipboard.readText until the expected URL fragment appears or a
timeout elapses) so the test waits for the clipboard to be populated before
asserting the expected story link.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
code/e2e-tests/manager.spec.ts (4)

11-15: Grant clipboard permissions before navigation to deflake clipboard assertions

Without explicit permissions, navigator.clipboard.readText() is flaky across browsers. Grant permissions for the Storybook origin before page.goto().

Apply this diff:

   test.beforeEach(async ({ page }) => {
-    await page.goto(storybookUrl);
+    await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], {
+      origin: storybookUrl,
+    });
+    await page.goto(storybookUrl);
 
     await new SbPage(page, expect).waitUntilLoaded();
   });

58-101: Use polling for clipboard; keep context‑menu checks as-is

Great dev-only gating via test.skip. Replace direct clipboard read with expect.poll for stability.

Apply this diff:

-      await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
-        'Primary'
-      );
+      await expect
+        .poll(() => page.evaluate(() => navigator.clipboard.readText()))
+        .toContain('Primary');

102-114: Dev share test: poll clipboard result

The dev gating is correct. Swap to polling to avoid timing flakes after clicking “Copy story link”.

Apply this diff:

-      await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
-        `${storybookUrl}/?path=/story/example-button--primary`
-      );
+      await expect
+        .poll(() => page.evaluate(() => navigator.clipboard.readText()))
+        .toContain(`${storybookUrl}/?path=/story/example-button--primary`);

116-128: Build share test: poll clipboard result

Same clipboard flake here; use polling.

Apply this diff:

-      await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
-        `${storybookUrl}/?path=/story/example-button--primary`
-      );
+      await expect
+        .poll(() => page.evaluate(() => navigator.clipboard.readText()))
+        .toContain(`${storybookUrl}/?path=/story/example-button--primary`);
🧹 Nitpick comments (1)
code/e2e-tests/manager.spec.ts (1)

8-8: Name the env flag more explicitly

Consider renaming type to storybookType for clarity and to avoid confusion with the TS type keyword in declarations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af37fd7 and cc2ffba.

📒 Files selected for processing (1)
  • code/e2e-tests/manager.spec.ts (8 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
code/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

code/**/*.{test,spec}.{ts,tsx}: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx

Files:

  • code/e2e-tests/manager.spec.ts
🧬 Code graph analysis (1)
code/e2e-tests/manager.spec.ts (1)
code/e2e-tests/util.ts (1)
  • SbPage (8-203)
🔇 Additional comments (6)
code/e2e-tests/manager.spec.ts (6)

20-35: Settings tooltip test reads well

The visibility/hide flows (Escape and outside click) look solid.


49-49: UI trigger switch to Settings: LGTM

Switching the menu trigger to Settings aligns with the new UX.


150-150: Toolbar toggling via Settings: LGTM

Also applies to: 154-154


189-189: Panel toggling via Settings: LGTM


241-241: Fullscreen toggling via Settings: LGTM


269-269: Settings page navigation: LGTM

@yannbf yannbf merged commit 2d630a4 into next Sep 18, 2025
58 checks passed
@yannbf yannbf deleted the yann/open-in-editor-feature branch September 18, 2025 12:10
@github-actions github-actions bot mentioned this pull request Sep 18, 2025
16 tasks
@ndelangen ndelangen added the needs qa Indicates that this needs manual QA during the upcoming minor/major release label Sep 29, 2025
@ndelangen ndelangen removed the needs qa Indicates that this needs manual QA during the upcoming minor/major release label Oct 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.