Skip to content

Core: Honor BROWSER shell scripts before xdg-open#33292

Merged
ndelangen merged 10 commits into
storybookjs:nextfrom
robbchar:fix/browser-script-bypass-xdg
Jan 12, 2026
Merged

Core: Honor BROWSER shell scripts before xdg-open#33292
ndelangen merged 10 commits into
storybookjs:nextfrom
robbchar:fix/browser-script-bypass-xdg

Conversation

@robbchar
Copy link
Copy Markdown
Contributor

@robbchar robbchar commented Dec 5, 2025

Closes #32949

What I did

Modified the code to allow for more script types other than just .js. (As a disclaimer I can't test this in a reproduction in my current environment, Windows Home...)

Checklist for Contributors

Testing

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

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

Manual testing

As I said previously I can't test this in my local environment, Windows Home... I used Github Codespaces to repro the bug I just can't easily run against a fix.

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 PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

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

Summary by CodeRabbit

  • New Features

    • Added shell-script support and broader script recognition for launching browsers (.sh, .js, .ts, .mjs, .cjs).
    • Exposed a new environment error type for clearer failure reporting.
  • Bug Fixes / Reliability

    • Improved environment detection, unified execution flow, and consistent logging for script/browser launch failures; preserved existing platform-specific fallbacks.
  • Tests

    • Added unit tests validating launch behavior across platforms and script vs. browser scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

Comment on lines +41 to +43
value.toLowerCase().endsWith('.sh')
) {
action = Actions.SCRIPT;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Attn: this will cause a shell script to be passed on to a Node runtime. It will most likely cause bugs for other users.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, definitely, that was the bug though, right? #32949 My thought was that the reporter has that setup intentionally so that something happens in addition to the browser running.

Copy link
Copy Markdown
Contributor

@Sidnioulz Sidnioulz Dec 8, 2025

Choose a reason for hiding this comment

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

They want it to be called, but probably not by a Node runtime. Rather, by a Shell runtime. xdg-open will not run in Node.

I believe the way to handle this would be to add an Actions type for shell and to spawn a shell interpreter. Which leads to the question: what operating systems are expected to have .sh? Should POSIX be assumed? Should the presence of bash be assumed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good questions. They weren't specific with the use-case, I was thinking the user would deal with the actual running of the script and the things within the script. I bet storybook doesn't really want to be in the business of figuring out OS's/shells that kind of thing.

If we do want to go down that route then I feel like we could decide things like what's assumed, Though actually spawning off a node script, or trying to, seems like pretty good 'best effort' for something that is not the main purpose of storybook.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree that passing .sh to Node would be wrong; we either need a shell branch or drop .sh from this path. Since Storybook likely doesn’t want to manage cross-platform shell execution, I propose we support Node-run scripts only and remove .sh here.

Copy link
Copy Markdown
Contributor

@Sidnioulz Sidnioulz Dec 9, 2025

Choose a reason for hiding this comment

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

If the goal of the issue is to support xdg-open users, who rely on FreeDesktop.org standards (including most Linux and some BSD users), then surely it's fine to restrict .sh execution to POSIX compliant systems e.g. Linux, BSD, Mac and WSL. We can just call bash like we call node (even though users could prefer another JS interpreter).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh wait, so I said 'thumbs up' but I thought of something else. This is for what I would call 'xdg-open'-like users in that they don't have xdg-open but rather some script, that presumably acts in the same way. I'm still going to implement it to work POSIX systems.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 9, 2025

📝 Walkthrough

Walkthrough

Refactors browser-launch logic: adds an Actions enum with SHELL_SCRIPT, a BrowserEnvError class, discriminated browser-target types (NONE, BROWSER, SCRIPT, SHELL_SCRIPT), centralized child-process event handling, executeNodeScript/executeShellScript helpers, platform shell checks, and new unit tests for these flows. (33 words)

Changes

Cohort / File(s) Change Summary
Browser opener utility enhancement
code/core/src/core-server/utils/open-browser/opener.ts
Introduced Actions enum including SHELL_SCRIPT; added discriminated browser env return type (NONE/BROWSER/SCRIPT/SHELL_SCRIPT) and default BROWSER; exported BrowserEnvError; added attachEventHandlers, executeNodeScript, executeShellScript; preserved AppleScript/Darwin handling; replaced console logs with logger.error; adjusted openBrowser control flow with canRunShell gating and updated error handling.
Unit tests for opener
code/core/src/core-server/utils/open-browser/opener.test.ts
New test file validating openBrowser behavior when BROWSER points to node scripts (.js/.mjs/.cjs/.ts) and shell scripts (.sh); mocks cross-spawn and open; covers Windows fallback and Unix (/bin/sh) execution and platform/env variations.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant openBrowser
    participant EnvCheck as getBrowserEnv
    participant NodeExec as executeNodeScript
    participant ShellExec as executeShellScript
    participant Opener as open (system/open package)

    Caller->>openBrowser: openBrowser(startUrl)
    openBrowser->>EnvCheck: inspect BROWSER env -> browserTarget

    alt browserTarget == SCRIPT
        EnvCheck->>openBrowser: SCRIPT(scriptPath)
        openBrowser->>NodeExec: spawn node [scriptPath, url]
        NodeExec-->>openBrowser: success / failure (via attachEventHandlers)
    else browserTarget == SHELL_SCRIPT and canRunShell == true
        EnvCheck->>openBrowser: SHELL_SCRIPT(scriptPath)
        openBrowser->>ShellExec: spawn /bin/sh [scriptPath, url]
        ShellExec-->>openBrowser: success / failure (via attachEventHandlers)
    else browserTarget == SHELL_SCRIPT and canRunShell == false
        EnvCheck->>openBrowser: SHELL_SCRIPT(scriptPath)
        openBrowser->>Opener: open(url) as fallback (no spawn)
        Opener-->>openBrowser: result
    else browserTarget == BROWSER
        EnvCheck->>openBrowser: BROWSER(appName)
        openBrowser->>Opener: open(url, {app: appName})
        Opener-->>openBrowser: result
    else browserTarget == NONE or error
        EnvCheck-->>openBrowser: BrowserEnvError
        openBrowser-->>Caller: throw BrowserEnvError
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Files/areas to focus on:
    • spawn argument construction and platform-specific quoting/escaping in executeNodeScript / executeShellScript
    • correctness of attachEventHandlers (error vs exit handling) and use of logger.error
    • getBrowserEnv discrimination logic and default BROWSER resolution
    • places where catch blocks were changed to catch without an error variable and where BrowserEnvError is thrown
    • tests' platform/env mocks and their fidelity to runtime behavior

Possibly related PRs

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
code/core/src/core-server/utils/open-browser/opener.ts (1)

50-65: Function name is misleading for shell script support.

The function name executeNodeScript accurately reflects that it spawns scripts using Node.js (process.execPath), but the changes at lines 37-43 now route shell scripts here. If shell script support is added, this function should either:

  • Be renamed to reflect generic script execution (e.g., executeScript)
  • Detect file extension and dispatch to appropriate runtime
  • Remain Node-specific while shell scripts are handled by a separate function

This comment relates to the critical issue raised at lines 37-43.

🧹 Nitpick comments (1)
code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts (1)

35-44: Add test coverage for .mjs and .cjs extensions.

The changes in opener.ts added support for .mjs and .cjs extensions, but only .sh is tested here. Since .mjs and .cjs are valid Node.js module formats that should work with the current implementation, add test cases to verify they execute correctly via executeNodeScript.

Example additional tests:

it('executes .mjs script specified via BROWSER', () => {
  process.env.BROWSER = '/tmp/browser.mjs';
  openBrowser('http://localhost:6006/');

  expect(vi.mocked(spawn)).toHaveBeenCalledWith(
    process.execPath,
    ['/tmp/browser.mjs', 'http://localhost:6006/'],
    { stdio: 'inherit' }
  );
  expect(vi.mocked(open)).not.toHaveBeenCalled();
});

it('executes .cjs script specified via BROWSER', () => {
  process.env.BROWSER = '/tmp/browser.cjs';
  openBrowser('http://localhost:6006/');

  expect(vi.mocked(spawn)).toHaveBeenCalledWith(
    process.execPath,
    ['/tmp/browser.cjs', 'http://localhost:6006/'],
    { stdio: 'inherit' }
  );
  expect(vi.mocked(open)).not.toHaveBeenCalled();
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e7805b and bddce0a.

📒 Files selected for processing (2)
  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts (1 hunks)
  • code/core/src/core-server/utils/open-browser/opener.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESLint and Prettier configurations that are enforced in the codebase

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{ts,tsx,js,jsx,mjs}: Use server-side logger from 'storybook/internal/node-logger' for Node.js code
Use client-side logger from 'storybook/internal/client-logger' for browser code
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
code/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions that need to be tested from their modules

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
code/**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier with --write flag to format code before committing
Run ESLint with yarn lint:js:cmd to check for linting issues and fix errors before committing

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{test,spec}.{ts,tsx}: Test files should follow the naming pattern *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx
Follow the spy mocking rules defined in .cursor/rules/spy-mocking.mdc for consistent mocking patterns with Vitest

Files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.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 the mocked functions in Vitest tests
Implement mock behaviors in beforeEach blocks in Vitest tests
Mock all required dependencies that the test subject uses
Each mock implementation should return a Promise for async functions in Vitest
Mock implementations should match the expected return type of the original function
Mock all required properties and methods that the test subject uses in Vitest tests
Avoid direct function mocking without vi.mocked() in Vitest tests
Avoid mock implementations outside of beforeEach blocks in Vitest tests
Avoid mocking without the spy: true option in Vitest tests
Avoid inline mock implementations within test cases in Vitest tests
Avoid mocking only a subset of required dependencies in Vitest tests
Mock at the highest level of abstraction needed in Vitest tests
Keep mock implementations simple and focused in Vitest tests
Use type-safe mocking with vi.mocked() in Vitest tests
Document complex mock behaviors in Vitest tests
Group related mocks together in Vitest tests

Files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
code/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{test,spec}.{ts,tsx,js,jsx}: Write meaningful unit tests that actually import and call the functions being tested
Mock external dependencies using vi.mock() for file system, loggers, and other external dependencies in tests
Aim for high test coverage of business logic (75%+ for statements/lines) using coverage reports

Files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
🧠 Learnings (15)
📚 Learning: 2025-11-24T17:49:31.838Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T17:49:31.838Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Follow the spy mocking rules defined in `.cursor/rules/spy-mocking.mdc` for consistent mocking patterns with Vitest

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Document complex mock behaviors in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{test,spec}.{ts,tsx,js,jsx} : Mock external dependencies using vi.mock() for file system, loggers, and other external dependencies in tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Implement mock behaviors in `beforeEach` blocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mocking only a subset of required dependencies in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Group related mocks together in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid inline mock implementations within test cases in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock all required properties and methods that the test subject uses in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{test,spec}.{ts,tsx,js,jsx} : Write meaningful unit tests that actually import and call the functions being tested

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Keep mock implementations simple and focused in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use `vi.mock()` with the `spy: true` option for all package and file mocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use `vi.mocked()` to type and access the mocked functions in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid direct function mocking without `vi.mocked()` in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use type-safe mocking with `vi.mocked()` in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mock implementations outside of `beforeEach` blocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts

Comment on lines +10 to +11
vi.mock('open', () => ({ default: vi.fn() }));
vi.mock('cross-spawn', () => ({ default: vi.fn() }));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add spy: true option to mock calls.

Per coding guidelines, all package mocks in Vitest tests must use the spy: true option for consistent mocking patterns.

Apply this diff:

-vi.mock('open', () => ({ default: vi.fn() }));
-vi.mock('cross-spawn', () => ({ default: vi.fn() }));
+vi.mock('open', { spy: true });
+vi.mock('cross-spawn', { spy: true });

Then update the beforeEach mock implementations at lines 24-26 to configure the spied functions:

beforeEach(() => {
  vi.resetAllMocks();
  process.env = { ...originalEnv, BROWSER: '/tmp/browser.sh' };
  process.argv = ['node', 'test'];
  Object.defineProperty(process, 'platform', { value: 'linux' });

  vi.mocked(open).mockResolvedValue({} as unknown as Awaited<ReturnType<typeof open>>);
  const child = { on: vi.fn() } as unknown as ChildProcess;
  vi.mocked(spawn).mockReturnValue(child);
});

As per coding guidelines for Vitest spy mocking.

🤖 Prompt for AI Agents
In code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts around
lines 10-11 and updating beforeEach at lines 24-26, the mocks for 'open' and
'cross-spawn' need the spy: true option and the test setup must configure the
spied functions; change vi.mock calls to include { spy: true }, then in
beforeEach replace the previous mock implementations with calls that configure
the spied functions: use vi.mocked(open).mockResolvedValue(...) and
vi.mocked(spawn).mockReturnValue(...) after resetting mocks and setting
process.env/argv/platform as shown in the review comment. Ensure types are
preserved (cast return values as needed) and remove any direct vi.fn()
replacements so the spy-based mocks are used.

Comment thread code/core/src/core-server/utils/open-browser/__tests__/opener.test.ts Outdated
Comment thread code/core/src/core-server/utils/open-browser/opener.ts Outdated
@storybook-app-bot
Copy link
Copy Markdown

storybook-app-bot Bot commented Dec 9, 2025

Package Benchmarks

Commit: 8f35a87, ran on 12 January 2026 at 08:32:49 UTC

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

storybook

Before After Difference
Dependency count 49 49 0
Self size 20.22 MB 20.19 MB 🎉 -33 KB 🎉
Dependency size 16.52 MB 16.52 MB 0 B
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 183 183 0
Self size 775 KB 775 KB 0 B
Dependency size 67.29 MB 67.26 MB 🎉 -31 KB 🎉
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 176 176 0
Self size 30 KB 30 KB 0 B
Dependency size 65.87 MB 65.84 MB 🎉 -31 KB 🎉
Bundle Size Analyzer Link Link

create-storybook

Before After Difference
Dependency count 50 50 0
Self size 999 KB 999 KB 0 B
Dependency size 36.74 MB 36.70 MB 🎉 -33 KB 🎉
Bundle Size Analyzer node node

@robbchar
Copy link
Copy Markdown
Contributor Author

robbchar commented Dec 9, 2025

In addition to the CR changes I also moved the test file to be collocated with it's test file to be consistent with other test files.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Dec 9, 2025

View your CI Pipeline Execution ↗ for commit e71775a


☁️ Nx Cloud last updated this comment at 2025-12-09 21:49:37 UTC

Copy link
Copy Markdown
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
code/core/src/core-server/utils/open-browser/opener.ts (1)

83-93: Replace console.log with node-logger.

Per coding guidelines, Node.js code should use the server-side logger from storybook/internal/node-logger instead of console.log.

+import { logger } from 'storybook/internal/node-logger';

Then update the handler:

 function attachCloseHandler(child: ReturnType<typeof spawn>, scriptPath: string) {
   child.on('close', (code) => {
     if (code !== 0) {
-      console.log();
-      console.log(picocolors.red('The script specified as BROWSER environment variable failed.'));
-      console.log(`${picocolors.cyan(scriptPath)} exited with code ${code}.`);
-      console.log();
+      logger.error(
+        `The script specified as BROWSER environment variable failed.\n${picocolors.cyan(scriptPath)} exited with code ${code}.`
+      );
       return;
     }
   });
 }
🧹 Nitpick comments (2)
code/core/src/core-server/utils/open-browser/opener.ts (2)

39-39: Use ES import for consistency.

The file uses ES module imports elsewhere. Consider importing fs at the top of the file for consistency with the codebase style.

 import { execSync } from 'node:child_process';
+import { readFileSync } from 'node:fs';
 import { join } from 'node:path';

Then update line 39:

-    const version = require('fs').readFileSync('/proc/version', 'utf8').toLowerCase();
+    const version = readFileSync('/proc/version', 'utf8').toLowerCase();

217-225: Clarify fallback behavior for shell scripts on Windows.

When canRunShell is false (native Windows), line 224 passes the .sh path to startBrowserProcess as the browser app. This relies on the open library to handle it, which may have unpredictable results on Windows (could open in a text editor, fail silently, or prompt for an application).

Consider either:

  1. Adding a warning/log when falling back on Windows to inform users their shell script won't execute as intended, or
  2. Returning false to indicate the browser couldn't be opened as configured.
     case Actions.SHELL_SCRIPT: {
       if (!value) {
         throw new BrowserEnvError('BROWSER environment variable is not set.');
       }
       if (canRunShell) {
         return executeShellScript(value, url);
       }
+      logger.warn(
+        'Shell scripts cannot be executed natively on Windows. Falling back to default browser behavior.'
+      );
       return startBrowserProcess(browserTarget, url, args);
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e71775a and 29a62fd.

📒 Files selected for processing (2)
  • code/core/src/core-server/utils/open-browser/opener.test.ts (1 hunks)
  • code/core/src/core-server/utils/open-browser/opener.ts (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/core/src/core-server/utils/open-browser/opener.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESLint and Prettier configurations that are enforced in the codebase

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{ts,tsx,js,jsx,mjs}: Use server-side logger from 'storybook/internal/node-logger' for Node.js code
Use client-side logger from 'storybook/internal/client-logger' for browser code
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions that need to be tested from their modules

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier with --write flag to format code before committing
Run ESLint with yarn lint:js:cmd to check for linting issues and fix errors before committing

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
🧠 Learnings (3)
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Follow existing patterns and conventions in the Storybook codebase

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-09-17T07:31:04.432Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32484
File: code/core/package.json:326-326
Timestamp: 2025-09-17T07:31:04.432Z
Learning: In Storybook's core package, dependencies like `open` are bundled into the final distribution during the build process, so they should remain in devDependencies rather than being moved to dependencies. End users don't need these packages as separate runtime dependencies since they're included in the bundled code.

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-09-17T07:32:14.512Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32484
File: code/core/src/core-server/utils/open-in-browser.ts:6-6
Timestamp: 2025-09-17T07:32:14.512Z
Learning: When reviewing async functions that perform side effects like opening browsers, `.catch()` is preferred over `await` if the operation should be non-blocking. This prevents unhandled Promise rejections while keeping the operation as fire-and-forget.

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
⏰ 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). (1)
  • GitHub Check: normal
🔇 Additional comments (3)
code/core/src/core-server/utils/open-browser/opener.ts (3)

58-68: LGTM!

The extension detection now correctly differentiates between Node.js scripts (.js, .mjs, .cjs) and shell scripts (.sh), addressing the original issue where shell scripts were incorrectly handled.


95-111: LGTM!

The separation of executeNodeScript and executeShellScript is clean. Using /bin/sh is appropriate for POSIX systems, and the canRunShell guard in openBrowser ensures this isn't called on native Windows.


72-81: Verify error code uniqueness for CORE_SERVER category.

The error class is well-structured, but the hardcoded code: 1 should be verified to ensure it doesn't conflict with other CORE_SERVER error codes elsewhere in the codebase. Check that this code value is either unique within the category or follows the established pattern for error code allocation in this category.

Comment thread code/core/src/core-server/utils/open-browser/opener.ts Outdated
Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (1)
code/core/src/core-server/utils/open-browser/opener.ts (1)

8-8: isWsl is effectively unused for real WSL and adds complexity to canRunShell

Because canRunShell is computed as process.platform !== 'win32' || isWsl(), isWsl() is only ever evaluated when process.platform === 'win32'. In real WSL, process.platform is 'linux', so the left side is already true and isWsl() is never consulted. That means:

  • All non‑Windows platforms (including WSL) already satisfy canRunShell via process.platform !== 'win32'.
  • isWsl() currently only runs on true Windows, where /proc/version typically doesn’t exist and the env flags aren’t set, so it just returns false after a failed read.

Functionally this still does what you want (shell scripts only on non‑win32), but the WSL detection logic is redundant and can be confusing, especially given the WSL‑specific tests. I’d either:

  • Simplify to const canRunShell = process.platform !== 'win32'; and drop isWsl and its tests, or
  • If you truly need WSL detection, rework canRunShell and isWsl along the usual pattern (isWsl checking process.platform === 'linux' plus /proc/version/env heuristics) so that WSL is actually identified by that helper.

This is a behavioral no‑op today but would reduce surprise for future maintainers.

How does Node.js report `process.platform` when running inside Windows Subsystem for Linux (WSL), and what’s the typical pattern libraries use to implement an `isWsl` helper?

Also applies to: 30-41, 198-221

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29a62fd and 395d167.

📒 Files selected for processing (2)
  • code/core/src/core-server/utils/open-browser/opener.test.ts (1 hunks)
  • code/core/src/core-server/utils/open-browser/opener.ts (6 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESLint and Prettier configurations that are enforced in the codebase

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{ts,tsx,js,jsx,mjs}: Use server-side logger from 'storybook/internal/node-logger' for Node.js code
Use client-side logger from 'storybook/internal/client-logger' for browser code
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
code/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions that need to be tested from their modules

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
code/**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier with --write flag to format code before committing
Run ESLint with yarn lint:js:cmd to check for linting issues and fix errors before committing

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{test,spec}.{ts,tsx}: Test files should follow the naming pattern *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx
Follow the spy mocking rules defined in .cursor/rules/spy-mocking.mdc for consistent mocking patterns with Vitest

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.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 the mocked functions in Vitest tests
Implement mock behaviors in beforeEach blocks in Vitest tests
Mock all required dependencies that the test subject uses
Each mock implementation should return a Promise for async functions in Vitest
Mock implementations should match the expected return type of the original function
Mock all required properties and methods that the test subject uses in Vitest tests
Avoid direct function mocking without vi.mocked() in Vitest tests
Avoid mock implementations outside of beforeEach blocks in Vitest tests
Avoid mocking without the spy: true option in Vitest tests
Avoid inline mock implementations within test cases in Vitest tests
Avoid mocking only a subset of required dependencies in Vitest tests
Mock at the highest level of abstraction needed in Vitest tests
Keep mock implementations simple and focused in Vitest tests
Use type-safe mocking with vi.mocked() in Vitest tests
Document complex mock behaviors in Vitest tests
Group related mocks together in Vitest tests

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
code/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{test,spec}.{ts,tsx,js,jsx}: Write meaningful unit tests that actually import and call the functions being tested
Mock external dependencies using vi.mock() for file system, loggers, and other external dependencies in tests
Aim for high test coverage of business logic (75%+ for statements/lines) using coverage reports

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
🧠 Learnings (17)
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{ts,tsx,js,jsx,mjs} : Use server-side logger from 'storybook/internal/node-logger' for Node.js code

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{ts,tsx,js,jsx,mjs} : Use client-side logger from 'storybook/internal/client-logger' for browser code

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-09-17T07:32:14.512Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32484
File: code/core/src/core-server/utils/open-in-browser.ts:6-6
Timestamp: 2025-09-17T07:32:14.512Z
Learning: When reviewing async functions that perform side effects like opening browsers, `.catch()` is preferred over `await` if the operation should be non-blocking. This prevents unhandled Promise rejections while keeping the operation as fire-and-forget.

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-24T17:49:31.838Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T17:49:31.838Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Follow the spy mocking rules defined in `.cursor/rules/spy-mocking.mdc` for consistent mocking patterns with Vitest

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{test,spec}.{ts,tsx,js,jsx} : Write meaningful unit tests that actually import and call the functions being tested

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Document complex mock behaviors in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mocking only a subset of required dependencies in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{test,spec}.{ts,tsx,js,jsx} : Mock external dependencies using vi.mock() for file system, loggers, and other external dependencies in tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Keep mock implementations simple and focused in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Group related mocks together in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid inline mock implementations within test cases in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:31.838Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T17:49:31.838Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Test files should follow the naming pattern `*.test.ts`, `*.test.tsx`, `*.spec.ts`, or `*.spec.tsx`

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock all required properties and methods that the test subject uses in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Implement mock behaviors in `beforeEach` blocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use `vi.mock()` with the `spy: true` option for all package and file mocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use `vi.mocked()` to type and access the mocked functions in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid direct function mocking without `vi.mocked()` in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
🧬 Code graph analysis (1)
code/core/src/core-server/utils/open-browser/opener.test.ts (1)
code/core/src/core-server/utils/open-browser/opener.ts (1)
  • openBrowser (197-229)
⏰ 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). (1)
  • GitHub Check: normal
🔇 Additional comments (2)
code/core/src/core-server/utils/open-browser/opener.ts (1)

11-19: Structured error handling and logging for script execution look good

Using a dedicated BrowserEnvError, centralized attachCloseHandler, and logger.error for non‑zero exit codes keeps failures from BROWSER scripts visible without breaking the main server flow. The boolean return contract of executeNodeScript / executeShellScript and the non‑blocking open(url, options).catch(() => {}) pattern in startBrowserProcess are consistent with existing behavior and prior guidance for fire‑and‑forget browser launches. As per coding guidelines, using storybook/internal/node-logger instead of console is also correct here.

Also applies to: 69-89, 91-107

code/core/src/core-server/utils/open-browser/opener.test.ts (1)

3-12: Good, focused coverage of BROWSER script behaviors with consistent mocking

The test suite cleanly isolates BROWSER script handling across Node script extensions and shell-script scenarios, and the mocks for open and cross-spawn follow the project’s Vitest spy‑mocking rules (vi.mock(..., { spy: true }), central setup in beforeEach, and vi.mocked() usage). This gives solid regression coverage for the new SCRIPT / SHELL_SCRIPT flows without over‑specifying internal details. Based on learnings, this aligns well with the repo’s testing and mocking guidelines.

Also applies to: 18-29, 37-138

Comment thread code/core/src/core-server/utils/open-browser/opener.test.ts Outdated
Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (1)
code/core/src/core-server/utils/open-browser/opener.ts (1)

185-213: openBrowser control flow now properly prioritizes scripts and shells before xdg-open/open

The updated switch cleanly routes:

  • SCRIPT → Node script via executeNodeScript
  • SHELL_SCRIPT/bin/sh on non‑Windows, or legacy browser-opening behavior on Windows
  • BROWSER/default → existing startBrowserProcess path

This satisfies the regression’s requirement: honor script-like BROWSER values (including .sh) before falling back to mechanisms like xdg-open. One minor follow-up you might consider (not blocking) is updating the function JSDoc to mention shell scripts and the possibility of BrowserEnvError being thrown, since it currently only references node.js scripts and boolean returns.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea81187 and fdaca94.

📒 Files selected for processing (2)
  • code/core/src/core-server/utils/open-browser/opener.test.ts (1 hunks)
  • code/core/src/core-server/utils/open-browser/opener.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/core/src/core-server/utils/open-browser/opener.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESLint and Prettier configurations that are enforced in the codebase

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{ts,tsx,js,jsx,mjs}: Use server-side logger from 'storybook/internal/node-logger' for Node.js code
Use client-side logger from 'storybook/internal/client-logger' for browser code
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions that need to be tested from their modules

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier with --write flag to format code before committing
Run ESLint with yarn lint:js:cmd to check for linting issues and fix errors before committing

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
🧠 Learnings (3)
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{ts,tsx,js,jsx,mjs} : Use server-side logger from 'storybook/internal/node-logger' for Node.js code

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{ts,tsx,js,jsx,mjs} : Use client-side logger from 'storybook/internal/client-logger' for browser code

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-09-17T07:32:14.512Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32484
File: code/core/src/core-server/utils/open-in-browser.ts:6-6
Timestamp: 2025-09-17T07:32:14.512Z
Learning: When reviewing async functions that perform side effects like opening browsers, `.catch()` is preferred over `await` if the operation should be non-blocking. This prevents unhandled Promise rejections while keeping the operation as fire-and-forget.

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
⏰ 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). (1)
  • GitHub Check: normal
🔇 Additional comments (5)
code/core/src/core-server/utils/open-browser/opener.ts (5)

8-19: Imports and logger usage look consistent with server-side guidelines

Using storybook/internal/node-logger for this Node.js utility matches the logging guidance for code/**/*.{ts,tsx,js,jsx,mjs} and keeps output consistent with the rest of core-server.


23-54: BROWSER env classification for script vs shell looks correct but clearly POSIX-focused

The extended Actions enum and getBrowserEnv logic cleanly distinguish JS-family scripts (.js/.mjs/.cjs) from shell scripts (.sh), while preserving the default BROWSER behavior for all other values. This matches the linked issue’s intent: treat script-like BROWSER paths specially before falling back to the normal browser-launch flow, and only treat .sh as a shell script when explicitly named as such.


56-65: BrowserEnvError is a reasonable specialization for env-related failures

Wrapping these failures in a dedicated BrowserEnvError that extends StorybookError (with category: 'CORE_SERVER') is a good way to make browser-env issues explicit and distinguishable from other errors.


87-94: Shell execution via /bin/sh gated by canRunShell is aligned with the POSIX-only goal

Using /bin/sh as the interpreter for .sh BROWSER values on non‑Windows platforms is a pragmatic, portable choice and matches the discussion about limiting shell support to POSIX/WSL environments; the Windows case correctly falls back to the existing browser path instead of trying to run a shell script directly.


147-149: Non-blocking open with guarded catch matches existing async-side-effect guidance

The parameterless catch blocks here keep the behavior simple: errors from the AppleScript attempt and from open(url, options) are swallowed so browser-opening remains best-effort and non-fatal, while open(...).catch(() => {}) avoids unhandled promise rejections. This is consistent with the “fire-and-forget” guidance for open-browser flows.

Also applies to: 171-176

Comment thread code/core/src/core-server/utils/open-browser/opener.ts Outdated
Copy link
Copy Markdown
Contributor

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

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

Thanks @robbchar! Even though we were all looking at the wrong thing initially, there are a bunch of improvements in your PR that I'd definitely like to see merged.

See my comments on the issue and my other PR for full context :) I think we can focus your PR on adding tests and making JS handling more robust, and let the other one handle xdg-open and shell scripting.

});

it('executes a node script when BROWSER points to a JS file', () => {
process.env.BROWSER = '/tmp/browser.js';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@ndelangen do we have an env mocking library already? I've used mockedEnv in the past. I'm concerned we're not cleaning up env here so this could lead to future tests on the same runner behaving differently.

Copy link
Copy Markdown
Member

@ndelangen ndelangen Jan 7, 2026

Choose a reason for hiding this comment

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

No We do not have a unique library for that as far as I am aware, but you should be able to import process from node:process and mock it that way, like any other module.

If the code is local, that is...


function executeShellScript(scriptPath: string, url: string) {
const extraArgs = process.argv.slice(2);
const child = spawn('/bin/sh', [scriptPath, ...extraArgs, url], {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
const child = spawn('/bin/sh', [scriptPath, ...extraArgs, url], {
const child = spawn('sh', [scriptPath, ...extraArgs, url], {

POSIX does not mandate use of /bin/, but Node.js spawn should lookup commands through $PATH, ensuring that sh is found regardless of install location.

Comment thread code/core/src/core-server/utils/open-browser/opener.ts Outdated
Comment thread code/core/src/core-server/utils/open-browser/opener.ts
@yannbf yannbf removed their assignment Dec 15, 2025
@valentinpalkovic valentinpalkovic changed the title fix: honor BROWSER shell scripts before xdg-open #32949 Core: Honor BROWSER shell scripts before xdg-open Dec 15, 2025
@Sidnioulz
Copy link
Copy Markdown
Contributor

Hey @robbchar!

Now that #33325 is merged, could you please rebase your PR and refactor it to focus on detection Nodejs scripts and calling the Node runtime for them?

Thanks 🙏

@Sidnioulz Sidnioulz self-requested a review December 15, 2025 14:11
@robbchar robbchar force-pushed the fix/browser-script-bypass-xdg branch from e78fb9f to ae5c60d Compare December 15, 2025 23:20
@robbchar robbchar force-pushed the fix/browser-script-bypass-xdg branch from ae5c60d to e74681b Compare December 15, 2025 23:57
@robbchar robbchar force-pushed the fix/browser-script-bypass-xdg branch from e74681b to 6fea2b2 Compare December 16, 2025 00:05
Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (1)
code/core/src/core-server/utils/open-browser/opener.test.ts (1)

63-74: Consider adding test coverage for .ts extension.

The implementation supports .ts files for Node.js script execution (in anticipation of Node 24+ native TypeScript support), but there's no test case for it. Adding a test would ensure this path is covered.

Add a test case after the .cjs test:

it('executes a node script when BROWSER points to a TS file', () => {
  process.env.BROWSER = '/tmp/browser.ts';

  openBrowser('http://localhost:6006/');

  expect(vi.mocked(spawn)).toHaveBeenCalledWith(
    process.execPath,
    ['/tmp/browser.ts', 'http://localhost:6006/'],
    { stdio: 'inherit' }
  );
  expect(vi.mocked(open)).not.toHaveBeenCalled();
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e78fb9f and 6fea2b2.

📒 Files selected for processing (2)
  • code/core/src/core-server/utils/open-browser/opener.test.ts (1 hunks)
  • code/core/src/core-server/utils/open-browser/opener.ts (6 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{test,spec}.{ts,tsx}: Test files should follow the naming pattern *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx
Follow the spy mocking rules defined in .cursor/rules/spy-mocking.mdc for consistent mocking patterns with Vitest

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.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 the mocked functions in Vitest tests
Implement mock behaviors in beforeEach blocks in Vitest tests
Mock all required dependencies that the test subject uses
Each mock implementation should return a Promise for async functions in Vitest
Mock implementations should match the expected return type of the original function
Mock all required properties and methods that the test subject uses in Vitest tests
Avoid direct function mocking without vi.mocked() in Vitest tests
Avoid mock implementations outside of beforeEach blocks in Vitest tests
Avoid mocking without the spy: true option in Vitest tests
Avoid inline mock implementations within test cases in Vitest tests
Avoid mocking only a subset of required dependencies in Vitest tests
Mock at the highest level of abstraction needed in Vitest tests
Keep mock implementations simple and focused in Vitest tests
Use type-safe mocking with vi.mocked() in Vitest tests
Document complex mock behaviors in Vitest tests
Group related mocks together in Vitest tests

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESLint and Prettier configurations that are enforced in the codebase

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
  • code/core/src/core-server/utils/open-browser/opener.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{ts,tsx,js,jsx,mjs}: Use server-side logger from 'storybook/internal/node-logger' for Node.js code
Use client-side logger from 'storybook/internal/client-logger' for browser code
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions that need to be tested from their modules

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{test,spec}.{ts,tsx,js,jsx}: Write meaningful unit tests that actually import and call the functions being tested
Mock external dependencies using vi.mock() for file system, loggers, and other external dependencies in tests
Aim for high test coverage of business logic (75%+ for statements/lines) using coverage reports

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
code/**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier with --write flag to format code before committing
Run ESLint with yarn lint:js:cmd to check for linting issues and fix errors before committing

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
  • code/core/src/core-server/utils/open-browser/opener.ts
🧠 Learnings (21)
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{test,spec}.{ts,tsx,js,jsx} : Write meaningful unit tests that actually import and call the functions being tested

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:31.838Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T17:49:31.838Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Follow the spy mocking rules defined in `.cursor/rules/spy-mocking.mdc` for consistent mocking patterns with Vitest

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Document complex mock behaviors in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mocking only a subset of required dependencies in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{test,spec}.{ts,tsx,js,jsx} : Mock external dependencies using vi.mock() for file system, loggers, and other external dependencies in tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:31.838Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T17:49:31.838Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Test files should follow the naming pattern `*.test.ts`, `*.test.tsx`, `*.spec.ts`, or `*.spec.tsx`

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Keep mock implementations simple and focused in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid inline mock implementations within test cases in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Group related mocks together in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mocking without the `spy: true` option in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mock implementations outside of `beforeEach` blocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid direct function mocking without `vi.mocked()` in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use `vi.mock()` with the `spy: true` option for all package and file mocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Implement mock behaviors in `beforeEach` blocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use `vi.mocked()` to type and access the mocked functions in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock all required properties and methods that the test subject uses in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use type-safe mocking with `vi.mocked()` in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-09-17T07:32:14.512Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32484
File: code/core/src/core-server/utils/open-in-browser.ts:6-6
Timestamp: 2025-09-17T07:32:14.512Z
Learning: When reviewing async functions that perform side effects like opening browsers, `.catch()` is preferred over `await` if the operation should be non-blocking. This prevents unhandled Promise rejections while keeping the operation as fire-and-forget.

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to **/*.{ts,tsx} : Enable TypeScript strict mode

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{ts,tsx,js,jsx,mjs} : Use server-side logger from 'storybook/internal/node-logger' for Node.js code

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{ts,tsx,js,jsx,mjs} : Use client-side logger from 'storybook/internal/client-logger' for browser code

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
🧬 Code graph analysis (2)
code/core/src/core-server/utils/open-browser/opener.test.ts (1)
code/core/src/core-server/utils/open-browser/opener.ts (1)
  • openBrowser (190-222)
code/core/src/core-server/utils/open-browser/opener.ts (1)
code/core/src/server-errors.ts (1)
  • StorybookError (8-8)
⏰ 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). (1)
  • GitHub Check: normal
🔇 Additional comments (8)
code/core/src/core-server/utils/open-browser/opener.ts (6)

10-17: LGTM on imports.

The logger import from storybook/internal/node-logger correctly follows the coding guidelines for Node.js code, and StorybookError is properly imported for the custom error class.


41-52: LGTM on extended BROWSER detection.

The expanded extension detection properly routes Node.js scripts (.js, .mjs, .cjs, .ts) to Actions.SCRIPT and shell scripts (.sh) to Actions.SHELL_SCRIPT. The case-insensitive matching is appropriate.


56-65: LGTM on custom error class.

BrowserEnvError properly extends StorybookError with appropriate category and provides clear error messaging for BROWSER configuration issues.


67-82: LGTM on centralized event handling.

This properly addresses the concern about unhandled spawn errors by attaching both error and close listeners. The centralized approach keeps logging consistent across script execution paths.


93-100: LGTM on shell script execution.

Using 'sh' instead of '/bin/sh' correctly follows POSIX conventions—Node.js spawn will look up the shell interpreter via $PATH, ensuring portability across different Unix-like systems where sh may not be at /bin/sh.


206-214: LGTM on SHELL_SCRIPT handling with Windows fallback.

The platform check appropriately routes shell scripts to direct execution on POSIX systems and falls back to startBrowserProcess on Windows, which delegates to the OS's handling. This maintains the fire-and-forget behavior.

code/core/src/core-server/utils/open-browser/opener.test.ts (2)

10-11: LGTM on mock setup.

Using vi.mock() with { spy: true } correctly follows the coding guidelines for Vitest mocking patterns.


18-35: LGTM on test setup and cleanup.

The use of vi.spyOn(process, 'platform', 'get') correctly addresses the past review concern about platform mocking. Mock implementations are properly placed in beforeEach, and vi.restoreAllMocks() ensures cleanup.

Comment thread code/core/src/core-server/utils/open-browser/opener.test.ts
@robbchar
Copy link
Copy Markdown
Contributor Author

@Sidnioulz I'm sorry but I'm a bit confused should I be doing something like this:

if (hasNodeShebang(file)) {
  spawn(process.execPath, [file])
} else {
  open(file)
}

?

thanks

@Sidnioulz
Copy link
Copy Markdown
Contributor

@Sidnioulz I'm sorry but I'm a bit confused should I be doing something like this:

if (hasNodeShebang(file)) {
  spawn(process.execPath, [file])
} else {
  open(file)
}

?

thanks

I think you can skip that, as xdg-open is able to do it and we now know for sure that xdg-open is called by the package on all platforms :)

Copy link
Copy Markdown
Contributor

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

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

We've done some testing with @mrginglymus (thanks!) for Windows support of sh. Spawning works only on WSL, which returns a linux process.platform, so the condition used in the code is good.

Rather than call a browser and fail, I've taken the liberty to add a commit to your code to explicitly fail when running Shell on Windows. Specifically, I tell users to use WSL so they have a workaround available.

I've changed types a little bit too so we can avoid adding if's just to make TypeScript happy.

Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (2)
code/core/src/core-server/utils/open-browser/opener.ts (2)

216-238: Consider using exhaustive type checking for the switch statement.

The default case is unreachable with the current Actions enum. TypeScript can enforce exhaustiveness at compile time using a never-returning function, which would catch missing cases if the enum is extended.

🔎 Proposed refactor for exhaustive checking
+function assertNever(x: never): never {
+  throw new BrowserEnvError(`Unexpected action: ${x}`);
+}
+
 export function openBrowser(url: string) {
   const { action, value, args } = getBrowserEnv();
   // ...
   switch (action) {
     // ... existing cases ...
     case Actions.BROWSER: {
       return startBrowserProcess(browserTarget, url, args);
     }
     default: {
-      throw new BrowserEnvError('Not implemented.');
+      return assertNever(action);
     }
   }
 }

214-214: Consider moving the type cast inside the BROWSER case.

The as unknown as double cast is only used in the BROWSER case. Moving it inside that case would improve type safety and clarity.

🔎 Proposed refactor
-  const browserTarget = value as unknown as App | readonly App[] | undefined;
-
   switch (action) {
     // ...
     case Actions.BROWSER: {
-      return startBrowserProcess(browserTarget, url, args);
+      return startBrowserProcess(value as App | readonly App[] | undefined, url, args);
     }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6fea2b2 and 951f263.

📒 Files selected for processing (1)
  • code/core/src/core-server/utils/open-browser/opener.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESLint and Prettier configurations that are enforced in the codebase

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{ts,tsx,js,jsx,mjs}: Use server-side logger from 'storybook/internal/node-logger' for Node.js code
Use client-side logger from 'storybook/internal/client-logger' for browser code
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions that need to be tested from their modules

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
code/**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier with --write flag to format code before committing
Run ESLint with yarn lint:js:cmd to check for linting issues and fix errors before committing

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
🧠 Learnings (4)
📚 Learning: 2025-09-17T07:32:14.512Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32484
File: code/core/src/core-server/utils/open-in-browser.ts:6-6
Timestamp: 2025-09-17T07:32:14.512Z
Learning: When reviewing async functions that perform side effects like opening browsers, `.catch()` is preferred over `await` if the operation should be non-blocking. This prevents unhandled Promise rejections while keeping the operation as fire-and-forget.

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to **/*.{ts,tsx} : Enable TypeScript strict mode

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-24T17:49:31.838Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T17:49:31.838Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Test files should follow the naming pattern `*.test.ts`, `*.test.tsx`, `*.spec.ts`, or `*.spec.tsx`

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{ts,tsx,js,jsx,mjs} : Use client-side logger from 'storybook/internal/client-logger' for browser code

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
⏰ 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). (1)
  • GitHub Check: normal
🔇 Additional comments (5)
code/core/src/core-server/utils/open-browser/opener.ts (5)

87-102: LGTM!

Good centralized event handling for child processes. The error handler catches spawn failures while close handles non-zero exit codes, both using the logger as per coding guidelines.


104-111: LGTM!

Clean implementation that spawns Node.js with the script path and URL. The fire-and-forget pattern with event handlers aligns with the learning that .catch() / non-blocking is preferred for browser-opening operations.


113-120: LGTM!

Using 'sh' instead of '/bin/sh' correctly allows PATH lookup per POSIX standards, as discussed in past reviews. The implementation is consistent with executeNodeScript.


197-204: LGTM!

The .catch(() => {}) pattern correctly prevents unhandledRejection while maintaining fire-and-forget semantics, as per the retrieved learnings. The empty catch blocks are appropriate here since errors are intentionally ignored.


76-85: Error code 1 conflicts with another CORE_SERVER error.

Error codes are used for telemetry and must be unique. BrowserEnvError uses code 1, but SavingGlobalSettingsFileError in code/core/src/server-errors.ts already uses code 1 in the same category. Use code 5 instead, which is available in the sequence.

⛔ Skipped due to learnings
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{ts,tsx,js,jsx,mjs} : Use client-side logger from 'storybook/internal/client-logger' for browser code

Comment on lines +60 to +67
if (
value.toLowerCase().endsWith('.js') ||
value.toLowerCase().endsWith('.mjs') ||
value.toLowerCase().endsWith('.cjs') ||
value.toLowerCase().endsWith('.ts')
) {
return { action: Actions.SCRIPT, value, args };
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Document TypeScript runtime requirements for .ts script execution.

Executing .ts files requires Node.js v22.18.0+ with --experimental-strip-types flag, or v24+ for basic TypeScript (erasable syntax only). For TypeScript features like enums, --experimental-transform-types is required. Consider adding a version check or documenting this limitation, as the current code doesn't handle necessary runtime flags or fallbacks for earlier Node versions.

Copy link
Copy Markdown
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 (2)
code/core/src/core-server/utils/open-browser/opener.test.ts (1)

37-74: Consider adding test coverage for .ts files.

The implementation at opener.ts lines 60-67 supports .ts files for Node script execution, but no test covers this path. Adding a test would ensure this behavior is validated.

🔎 Suggested additional test
it('executes a node script when BROWSER points to a TS file', () => {
  process.env.BROWSER = '/tmp/browser.ts';

  openBrowser('http://localhost:6006/');

  expect(vi.mocked(spawn)).toHaveBeenCalledWith(
    process.execPath,
    ['/tmp/browser.ts', 'http://localhost:6006/'],
    { stdio: 'inherit' }
  );
  expect(vi.mocked(open)).not.toHaveBeenCalled();
});
code/core/src/core-server/utils/open-browser/opener.ts (1)

214-214: Unnecessary type cast introduces potential confusion.

The cast value as unknown as App | readonly App[] | undefined is applied but value is only used in startBrowserProcess when action is BROWSER. Since getBrowserEnv already returns value?: string for Actions.BROWSER, consider passing value directly to startBrowserProcess rather than pre-casting at the switch level. The App type from open expects string | readonly string[], so the cast is safe but could be clearer.

🔎 Suggested improvement
   const { action, value, args } = getBrowserEnv();
   // Returns win32 on PowerShell and Linux on WSL. Matches conditions when `sh` can be invoked.
   const canRunShell = process.platform !== 'win32';
-  const browserTarget = value as unknown as App | readonly App[] | undefined;

   switch (action) {
     // ...
     case Actions.BROWSER: {
-      return startBrowserProcess(browserTarget, url, args);
+      return startBrowserProcess(value, url, args);
     }

This removes the intermediate cast and lets startBrowserProcess handle the type appropriately.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 951f263 and 47285fe.

📒 Files selected for processing (2)
  • code/core/src/core-server/utils/open-browser/opener.test.ts (1 hunks)
  • code/core/src/core-server/utils/open-browser/opener.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESLint and Prettier configurations that are enforced in the codebase

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{ts,tsx,js,jsx,mjs}: Use server-side logger from 'storybook/internal/node-logger' for Node.js code
Use client-side logger from 'storybook/internal/client-logger' for browser code
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
code/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions that need to be tested from their modules

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
code/**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier with --write flag to format code before committing
Run ESLint with yarn lint:js:cmd to check for linting issues and fix errors before committing

Files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{test,spec}.{ts,tsx}: Test files should follow the naming pattern *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx
Follow the spy mocking rules defined in .cursor/rules/spy-mocking.mdc for consistent mocking patterns with Vitest

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.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 the mocked functions in Vitest tests
Implement mock behaviors in beforeEach blocks in Vitest tests
Mock all required dependencies that the test subject uses
Each mock implementation should return a Promise for async functions in Vitest
Mock implementations should match the expected return type of the original function
Mock all required properties and methods that the test subject uses in Vitest tests
Avoid direct function mocking without vi.mocked() in Vitest tests
Avoid mock implementations outside of beforeEach blocks in Vitest tests
Avoid mocking without the spy: true option in Vitest tests
Avoid inline mock implementations within test cases in Vitest tests
Avoid mocking only a subset of required dependencies in Vitest tests
Mock at the highest level of abstraction needed in Vitest tests
Keep mock implementations simple and focused in Vitest tests
Use type-safe mocking with vi.mocked() in Vitest tests
Document complex mock behaviors in Vitest tests
Group related mocks together in Vitest tests

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
code/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/*.{test,spec}.{ts,tsx,js,jsx}: Write meaningful unit tests that actually import and call the functions being tested
Mock external dependencies using vi.mock() for file system, loggers, and other external dependencies in tests
Aim for high test coverage of business logic (75%+ for statements/lines) using coverage reports

Files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
🧠 Learnings (20)
📚 Learning: 2025-09-17T07:32:14.512Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32484
File: code/core/src/core-server/utils/open-in-browser.ts:6-6
Timestamp: 2025-09-17T07:32:14.512Z
Learning: When reviewing async functions that perform side effects like opening browsers, `.catch()` is preferred over `await` if the operation should be non-blocking. This prevents unhandled Promise rejections while keeping the operation as fire-and-forget.

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to **/*.{ts,tsx} : Enable TypeScript strict mode

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-24T17:49:31.838Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T17:49:31.838Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Test files should follow the naming pattern `*.test.ts`, `*.test.tsx`, `*.spec.ts`, or `*.spec.tsx`

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Use Node.js version 22.21.1 as specified in .nvmrc

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{ts,tsx,js,jsx,mjs} : Use client-side logger from 'storybook/internal/client-logger' for browser code

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{test,spec}.{ts,tsx,js,jsx} : Write meaningful unit tests that actually import and call the functions being tested

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:31.838Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T17:49:31.838Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Follow the spy mocking rules defined in `.cursor/rules/spy-mocking.mdc` for consistent mocking patterns with Vitest

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Document complex mock behaviors in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mocking only a subset of required dependencies in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-28T14:50:24.889Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-28T14:50:24.889Z
Learning: Applies to code/**/*.{test,spec}.{ts,tsx,js,jsx} : Mock external dependencies using vi.mock() for file system, loggers, and other external dependencies in tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Keep mock implementations simple and focused in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid inline mock implementations within test cases in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Group related mocks together in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mocking without the `spy: true` option in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mock implementations outside of `beforeEach` blocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid direct function mocking without `vi.mocked()` in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use `vi.mock()` with the `spy: true` option for all package and file mocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Implement mock behaviors in `beforeEach` blocks in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use `vi.mocked()` to type and access the mocked functions in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock all required properties and methods that the test subject uses in Vitest tests

Applied to files:

  • code/core/src/core-server/utils/open-browser/opener.test.ts
🧬 Code graph analysis (2)
code/core/src/core-server/utils/open-browser/opener.ts (1)
code/core/src/server-errors.ts (1)
  • StorybookError (8-8)
code/core/src/core-server/utils/open-browser/opener.test.ts (1)
code/core/src/core-server/utils/open-browser/opener.ts (2)
  • openBrowser (210-239)
  • BrowserEnvError (76-85)
⏰ 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). (1)
  • GitHub Check: normal
🔇 Additional comments (10)
code/core/src/core-server/utils/open-browser/opener.test.ts (3)

1-11: Well-structured test setup following Vitest mocking guidelines.

The imports and mocks are correctly placed at the top of the file, using vi.mock() with the { spy: true } option as per coding guidelines. The test file properly imports the subject under test (openBrowser, BrowserEnvError) and its dependencies (spawn, open).


13-35: Clean test harness with proper environment isolation.

The setup correctly:

  • Uses vi.spyOn(process, 'platform', 'get') per past review guidance
  • Restores environment and mocks in afterEach
  • Implements mock behaviors in beforeEach as per coding guidelines

76-109: Tests correctly verify platform-specific behavior.

The Windows shell script test properly expects BrowserEnvError to be thrown, and the Linux test correctly expects 'sh' (not /bin/sh) aligning with the implementation and past review feedback.

code/core/src/core-server/utils/open-browser/opener.ts (7)

10-27: Proper use of server-side logger and well-defined Actions enum.

The import of logger from 'storybook/internal/node-logger' follows coding guidelines for Node.js code. The Actions enum provides clear discrimination for browser environment handling.


76-85: BrowserEnvError follows Storybook's error pattern correctly.

The error class properly extends StorybookError with consistent category, code, and name fields, enabling standardized error handling across the codebase.


87-102: Robust event handling for child processes.

The function properly handles both 'error' (spawn failures) and 'close' (non-zero exits) events, addressing the past review concern about hard crashes. Using logger.error instead of console follows coding guidelines.


104-120: Script execution helpers correctly implement fire-and-forget pattern.

Both functions properly use spawn with stdio: 'inherit', attach event handlers for error logging, and return true immediately. The use of 'sh' instead of /bin/sh follows POSIX compliance guidance from past reviews.


197-203: Fire-and-forget pattern with proper error suppression.

The .catch(() => {}) on the open() call correctly prevents unhandled Promise rejections while keeping the operation non-blocking, which aligns with project learnings for browser-opening side effects.


216-238: Comprehensive switch handling with exhaustive case coverage.

All Actions enum values are explicitly handled, and the default case provides a safety net with a clear error message. The Windows shell script error message helpfully suggests using WSL as an alternative.


60-67: Node.js 22.21.1 natively supports .ts execution without additional configuration.

The repository's Node.js v22.21.1 meets the minimum requirement for TypeScript type stripping. No runtime validation or documentation is needed to support .ts file execution in this project.

@robbchar
Copy link
Copy Markdown
Contributor Author

@Sidnioulz Those changes are fine, thanks :) So, is there anything else that I should do? Sorry, I 'looked away' for a bit but I'm back now so more available. I know you approved but if there is something I should do please let me know. thanks again

@Sidnioulz
Copy link
Copy Markdown
Contributor

@Sidnioulz Those changes are fine, thanks :) So, is there anything else that I should do? Sorry, I 'looked away' for a bit but I'm back now so more available. I know you approved but if there is something I should do please let me know. thanks again

You have done everything you needed to! Thanks again for your contribution! I'm waiting on core team members to be back from vacation to merge the PR.

@ndelangen
Copy link
Copy Markdown
Member

@robbchar Thank you for this PR, I think we're close to merging, but there a request I have.

Could you share as much details as possible on the steps to manually reproduce?
I understand you cannot run it locally, but you mention you did something on codespaces? Can you describe that procedure so we can later verify this fix?

@robbchar
Copy link
Copy Markdown
Contributor Author

Hello,

Sorry for the delayed reply,

The original repro was done in GitHub Codespaces via an “Open in Codespaces” flow from storybook.new, which no longer exists, so it’s no longer a one-click setup. I was able to recreate the environment manually, and here’s the process I used.

Codespaces repro steps

  1. Create a Codespace from the storybookjs/storybook repo.
  2. Switch to Node 18+ and enable Yarn 4:
nvm install 20
nvm use 20
corepack enable
corepack prepare yarn@4.10.3 --activate
  1. Create a sandbox using Storybook’s internal task generator:
sandbox --template react-vite/default-ts
(I ran this interactively; there may be a more direct non-interactive invocation, but this matches what I did.)
4. Set $BROWSER to a script and ensure xdg-open is not present (in the default Codespaces image it isn’t):
```bash
#!/usr/bin/env bash
echo "invoked: $@" >> /tmp/browser-invocations.log

Then

chmod +x /path/to/script
export BROWSER=/path/to/script
  1. Run Storybook from the generated sandbox:
cd ../storybook-sandboxes/react-vite-default-ts
yarn storybook
  1. It currently works for me, but the comment above says it will so I'm not surprised :)

Happy to provide more details or try additional variations if that helps.

Thanks!

@ndelangen ndelangen merged commit 39b8e53 into storybookjs:next Jan 12, 2026
59 checks passed
@ndelangen ndelangen added the needs qa Indicates that this needs manual QA during the upcoming minor/major release label Jan 12, 2026
@github-actions github-actions Bot mentioned this pull request Jan 12, 2026
19 tasks
@ndelangen ndelangen removed the needs qa Indicates that this needs manual QA during the upcoming minor/major release label Jan 20, 2026
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.

[Bug]: v10 new crash if $BROWSER points to .sh script (e.g. Codespaces)

5 participants