Repository: marp-team/marp-vscode
Pull request #478 (2024-12-31 17:47:03)

[v3] `markdown.marp.browser` and `markdown.marp.browserPath` settings
Marp CLI v4 has been supported Firefox browser. To support Firefox browser in the next major version of Marp for VS Code, this PR adds `markdown.marp.browser` and `markdown.marp.browserPath` settings.

### `markdown.marp.browser`

This option is coressponding to [`browser` option of Marp CLI](https://github.com/marp-team/marp-cli#choose-browser---browser).

- `auto`: Automatically detect Chrome, Chromium, Edge, or Firefox.
- `chrome`: Use Google Chrome.
- `edge`: Use Microsoft Edge.
- `firefox`: Use Mozilla Firefox.

### `markdown.marp.browserPath`

This option is to replace a legacy setting `markdown.marp.chromePath`. Set the custom browser path to export.

The kind of browser is specified by `markdown.marp.browser`. If it was `auto`, Marp for VS Code will detect the kind of browser automatically. Currently we simply detect if the binary name is including `firefox` or `fx`. 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e83211..67280c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,16 +13,20 @@
 ### Added
 
 - `markdown.marp.html` setting to control rendering HTML within Marp Markdown ([#476](https://github.com/marp-team/marp-vscode/pull/476))
-
-### Deprecated
-
-- `markdown.marp.enableHtml` setting ([#476](https://github.com/marp-team/marp-vscode/pull/476))
+- `markdown.marp.browser` and `markdown.marp.browserPath` settings to control internally using browser to export ([#478](https://github.com/marp-team/marp-vscode/pull/478))
+- Support Firefox as a browser for exporting ([#473](https://github.com/marp-team/marp-vscode/pull/473), [#474](https://github.com/marp-team/marp-vscode/pull/474), [#478](https://github.com/marp-team/marp-vscode/pull/478))
 
 ### Changed
 
+- Several allowed HTML elements through Marp Core are enabled by default ([#472](https://github.com/marp-team/marp-vscode/pull/472), [#474](https://github.com/marp-team/marp-vscode/pull/474), [#476](https://github.com/marp-team/marp-vscode/pull/476))
 - Upgrade development Node.js and dependent packages to the latest version ([#474](https://github.com/marp-team/marp-vscode/pull/474))
 - Migrate ESLint config to flat config ([#475](https://github.com/marp-team/marp-vscode/pull/475))
 
+### Deprecated
+
+- Deprecated `markdown.marp.enableHtml` setting in favor of `markdown.marp.html` ([#476](https://github.com/marp-team/marp-vscode/pull/476))
+- Deprecated `markdown.marp.chromePath` setting in favor of `markdown.marp.browserPath` ([#478](https://github.com/marp-team/marp-vscode/pull/478))
+
 ## v2.8.0 - 2023-10-28
 
 ### Changed
diff --git a/README.md b/README.md
index 69bd4a3..676c4f7 100644
--- a/README.md
+++ b/README.md
@@ -128,9 +128,13 @@ You can also execute command from the Command Palette (<kbd>F1</kbd> or <kbd>Ctr
 - **PNG** (_First slide only)_
 - **JPEG** (_First slide only)_
 
-Default file type can choose by `markdown.marp.exportType` preference.
+Default file type can choose by the `markdown.marp.exportType` setting.
 
-> ⚠️ Export except HTML requires to install any one of [Google Chrome](https://www.google.com/chrome/), [Chromium](https://www.chromium.org/), or [Microsoft Edge](https://www.microsoft.com/edge). You may also specify the custom path for Chrome / Chromium-based browser by preference `markdown.marp.chromePath`.
+> [!IMPORTANT]
+> Exporting PDF, PPTX, and image formats requires to install any one of [Google Chrome](https://www.google.com/chrome/), [Chromium](https://www.chromium.org/), [Microsoft Edge](https://www.microsoft.com/edge), or [Firefox](https://www.mozilla.org/firefox/). You may control using browser and the custom path for the browser by `markdown.marp.browser` and `markdown.marp.browserPath` settings.
+
+> [!NOTE]
+> A legacy setting `markdown.marp.chromePath` is deprecated since v2. Please use `markdown.marp.browserPath` instead.
 
 ### Use custom theme CSS 🛡️
 
@@ -175,7 +179,7 @@ Markdown preview will reload updated theme CSS automatically when you edited the
 
 ### Outline extension
 
-When Marp Markdown is enabled, you can use the extended [outline view](https://code.visualstudio.com/docs/languages/markdown#_outline-view) like following. They are enabled by default but you may disable by `markdown.marp.outlineExtension` preference.
+When Marp Markdown is enabled, you can use the extended [outline view](https://code.visualstudio.com/docs/languages/markdown#_outline-view) like following. They are enabled by default but you may disable by the `markdown.marp.outlineExtension` setting.
 
 #### Outline view for each slide
 
diff --git a/package.json b/package.json
index 9368c6b..5c0ff6f 100644
--- a/package.json
+++ b/package.json
@@ -115,10 +115,27 @@
             "Use inherited setting from `#markdown.preview.breaks#`."
           ]
         },
-        "markdown.marp.chromePath": {
+        "markdown.marp.browser": {
+          "type": "string",
+          "enum": [
+            "auto",
+            "chrome",
+            "edge",
+            "firefox"
+          ],
+          "default": "auto",
+          "description": "Controls the installed browser using internally to export PDF, PPTX, and the image.",
+          "enumDescriptions": [
+            "Automatically detect Chrome, Chromium, Edge, or Firefox.",
+            "Use Google Chrome.",
+            "Use Microsoft Edge.",
+            "Use Mozilla Firefox."
+          ]
+        },
+        "markdown.marp.browserPath": {
           "type": "string",
           "default": "",
-          "description": "Sets the custom path for Chrome or Chromium-based browser to export PDF, PPTX, and image. If it's empty, Marp will find out the installed Google Chrome / Chromium / Microsoft Edge."
+          "markdownDescription": "Configure the custom path for the installed browser using internally to export PDF, PPTX, and the image. The kind of browser is determined by `#markdown.marp.browser#`. When set to empty, Marp will find out a suitable installed browser automatically."
         },
         "markdown.marp.html": {
           "type": "string",
@@ -214,6 +231,12 @@
           "default": false,
           "description": "Enables all HTML elements in Marp Markdown. This setting is working only in the trusted workspace.",
           "deprecationMessage": "The setting \"markdown.marp.enableHtml\" is deprecated. Please use \"markdown.marp.html\" instead."
+        },
+        "markdown.marp.chromePath": {
+          "type": "string",
+          "default": "",
+          "description": "Sets the custom path for Chrome or Chromium-based browser to export PDF, PPTX, and image. If it's empty, Marp will find out the installed Google Chrome / Chromium / Microsoft Edge.",
+          "deprecationMessage": "The setting \"markdown.marp.chromePath\" is deprecated. Please use \"markdown.marp.browserPath\" instead."
         }
       }
     },
diff --git a/src/__mocks__/vscode.ts b/src/__mocks__/vscode.ts
index 3fe2ca4..bf96eb3 100644
--- a/src/__mocks__/vscode.ts
+++ b/src/__mocks__/vscode.ts
@@ -5,14 +5,18 @@ type MockedConf = Record<string, any>
 const defaultVSCodeVersion = 'v1.62.1'
 const defaultConf: MockedConf = {
   'markdown.marp.breaks': 'on',
-  'markdown.marp.chromePath': '',
-  'markdown.marp.enableHtml': false,
+  'markdown.marp.browser': 'auto',
+  'markdown.marp.browserPath': '',
   'markdown.marp.html': 'default',
   'markdown.marp.exportType': 'pdf',
   'markdown.marp.outlineExtension': true,
   'markdown.marp.pdf.noteAnnotations': false,
   'markdown.marp.pdf.outlines': 'off',
   'window.zoomLevel': 0,
+
+  // Legacy
+  'markdown.marp.chromePath': '',
+  'markdown.marp.enableHtml': false,
 }
 
 let currentConf: MockedConf = {}
diff --git a/src/commands/export.ts b/src/commands/export.ts
index 71c6ab4..86bedb7 100644
--- a/src/commands/export.ts
+++ b/src/commands/export.ts
@@ -45,6 +45,13 @@ const descriptions = {
   [Types.jpeg]: 'JPEG image (first slide only)' as const,
 }
 
+const browsers = {
+  chrome: '[Google Chrome](https://www.google.com/chrome/)',
+  chromium: '[Chromium](https://www.chromium.org/)',
+  edge: '[Microsoft Edge](https://www.microsoft.com/edge)',
+  firefox: '[Mozilla Firefox](https://www.mozilla.org/firefox/)',
+} as const
+
 export const ITEM_CONTINUE_TO_EXPORT = 'Continue to export...'
 export const ITEM_MANAGE_WORKSPACE_TRUST = 'Manage Workspace Trust...'
 
@@ -140,9 +147,51 @@ export const doExport = async (uri: Uri, document: TextDocument) => {
       })
 
       try {
-        await marpCli(['-c', conf.path, input.path, '-o', outputPath], {
-          baseUrl,
-        })
+        await marpCli(
+          ['-c', conf.path, input.path, '-o', outputPath],
+          { baseUrl },
+          {
+            onCLIError: ({ error, codes }) => {
+              if (error.errorCode === codes.NOT_FOUND_BROWSER) {
+                // Throw error with user-friendly instructions based on the current configuration
+                const browserOption = marpConfiguration().get<string>('browser')
+                const suggestBrowsers: string[] = []
+
+                switch (browserOption) {
+                  case 'chrome':
+                    suggestBrowsers.push(
+                      ...[
+                        browsers.chrome,
+                        process.platform === 'linux' ? browsers.chromium : '',
+                      ].filter((b) => !!b),
+                    )
+                    break
+                  case 'edge':
+                    suggestBrowsers.push(browsers.edge)
+                    break
+                  case 'firefox':
+                    suggestBrowsers.push(browsers.firefox)
+                    break
+                  default:
+                    suggestBrowsers.push(
+                      ...[
+                        browsers.chrome,
+                        process.platform === 'linux' ? browsers.chromium : '',
+                        browsers.edge,
+                        browsers.firefox,
+                      ].filter((b) => !!b),
+                    )
+                }
+
+                throw new MarpCLIError(
+                  `It requires to install a suitable browser, ${suggestBrowsers
+                    .join(', ')
+                    .replace(/, ([^,]*)$/, ' or $1')} for exporting.`,
+                )
+              }
+            },
+          },
+        )
 
         if (outputToLocalFS) {
           env.openExternal(uri)
diff --git a/src/marp-cli.ts b/src/marp-cli.ts
index 43cc4ab..a0724c0 100644
--- a/src/marp-cli.ts
+++ b/src/marp-cli.ts
@@ -1,10 +1,14 @@
 import { tmpdir } from 'node:os'
 import path from 'node:path'
-import type { marpCli } from '@marp-team/marp-cli'
+import type {
+  marpCli,
+  CLIError as CLIErrorType,
+  CLIErrorCode as CLIErrorCodeType,
+} from '@marp-team/marp-cli'
 import { nanoid } from 'nanoid'
 import { TextDocument, Uri, workspace } from 'vscode'
 import { WorkFile, marpCoreOptionForCLI } from './option'
-import { marpConfiguration, writeFile, unlink } from './utils'
+import { writeFile, unlink } from './utils'
 
 const createCleanup = (target: Uri) => async () => {
   await unlink(target)
@@ -12,6 +16,15 @@ const createCleanup = (target: Uri) => async () => {
 
 export class MarpCLIError extends Error {}
 
+export interface RunMarpCLIOptions {
+  onCLIError?: (e: MarpCLIErrorHandler) => void
+}
+
+export interface MarpCLIErrorHandler {
+  error: CLIErrorType
+  codes: typeof CLIErrorCodeType
+}
+
 export async function createWorkFile(doc: TextDocument): Promise<WorkFile> {
   // Use a real file if posibble
   if (doc.uri.scheme === 'file' && !doc.isDirty) {
@@ -81,46 +94,25 @@ export async function createConfigFile(
 }
 
 export default async function runMarpCli(
-  ...[argv, opts]: Parameters<typeof marpCli>
+  argv: Parameters<typeof marpCli>[0],
+  opts?: Parameters<typeof marpCli>[1],
+  { onCLIError }: RunMarpCLIOptions = {},
 ): Promise<void> {
   console.info(`Execute Marp CLI [${argv.join(' ')}] (${JSON.stringify(opts)})`)
 
   const { marpCli, CLIError, CLIErrorCode } = await import(
     '@marp-team/marp-cli'
   )
-  const { CHROME_PATH } = process.env
 
   let exitCode: number
 
   try {
-    process.env.CHROME_PATH =
-      marpConfiguration().get<string>('chromePath') || CHROME_PATH
-
     exitCode = await marpCli(argv, opts)
   } catch (e) {
-    console.error(e)
-
-    if (
-      e instanceof CLIError &&
-      e.errorCode === CLIErrorCode.NOT_FOUND_CHROMIUM
-    ) {
-      const browsers = ['[Google Chrome](https://www.google.com/chrome/)']
-
-      if (process.platform === 'linux')
-        browsers.push('[Chromium](https://www.chromium.org/)')
-
-      browsers.push('[Microsoft Edge](https://www.microsoft.com/edge)')
-
-      throw new MarpCLIError(
-        `It requires to install ${browsers
-          .join(', ')
-          .replace(/, ([^,]*)$/, ' or $1')} for exporting.`,
-      )
-    }
+    if (e instanceof CLIError) onCLIError?.({ error: e, codes: CLIErrorCode })
 
+    console.error(e)
     throw e
-  } finally {
-    process.env.CHROME_PATH = CHROME_PATH
   }
 
   if (exitCode !== 0) {
diff --git a/src/option.ts b/src/option.ts
index 74e6862..d0e3e49 100644
--- a/src/option.ts
+++ b/src/option.ts
@@ -1,5 +1,6 @@
 import { tmpdir } from 'node:os'
 import path from 'node:path'
+import type { Config } from '@marp-team/marp-cli'
 import { MarpOptions } from '@marp-team/marp-core'
 import { Options } from 'markdown-it'
 import { nanoid } from 'nanoid'
@@ -18,6 +19,12 @@ export interface WorkFile {
   cleanup: () => Promise<void>
 }
 
+export interface ConfigForCLI extends Config {
+  vscode: {
+    themeFiles: WorkFile[]
+  }
+}
+
 let cachedPreviewOption: MarpOptions | undefined
 
 const breaks = (inheritedValue: boolean): boolean => {
@@ -112,11 +119,60 @@ export const marpCoreOptionForCLI = async (
 ) => {
   const confMdPreview = workspace.getConfiguration('markdown.preview', uri)
 
+  let browser = marpConfiguration().get<'auto' | 'chrome' | 'edge' | 'firefox'>(
+    'browser',
+  )
+
+  const browserPath = (() => {
+    const browserPath = marpConfiguration().get<string>('browserPath')
+    if (browserPath) {
+      // If `markdown.marp.browserPath` is `auto`, detect the kind of browser by the binary name
+      if (browser === 'auto') {
+        try {
+          const binaryName = path.basename(browserPath).toLowerCase()
+
+          if (binaryName.includes('firefox') || binaryName.includes('fx'))
+            browser = 'firefox'
+        } catch (e) {
+          console.error(e)
+          console.warn(
+            'Failed to detect the kind of browser by the binary name.',
+          )
+        }
+      }
+
+      return browserPath
+    }
+
+    // Legacy compatibility
+    const chromePath = marpConfiguration().get<string>('chromePath')
+    if (chromePath) {
+      // Show warning for legacy configuration
+      window
+        .showWarningMessage(
+          'The setting "markdown.marp.chromePath" is deprecated. Please use "markdown.marp.browserPath" instead. Please review your settings JSON to make silence this warning.',
+          'Open Extension Settings',
+        )
+        .then((selected) => {
+          if (selected) void openExtensionSettings()
+        })
+
+      // Force to use Chrome if `markdown.marp.chromePath` is set
+      browser = 'chrome'
+
+      return chromePath
+    }
+
+    return undefined
+  })()
+
   const baseOpts = {
     allowLocalFiles,
     pdfNotes,
     pdfOutlines: pdfOutlines(),
     html: html().value,
+    browser: browser || 'auto',
+    browserPath,
     options: {
       markdown: {
         breaks: breaks(!!confMdPreview.get<boolean>('breaks')),
@@ -125,8 +181,7 @@ export const marpCoreOptionForCLI = async (
       math: math(),
     },
     themeSet: [] as string[],
-    vscode: {} as Record<string, any>,
-  }
+  } as ConfigForCLI
 
   const workspaceFolder = workspace.getWorkspaceFolder(uri)
   const parentFolder = uri.scheme === 'file' && path.dirname(uri.fsPath)
@@ -166,7 +221,7 @@ export const marpCoreOptionForCLI = async (
   ).filter((w): w is WorkFile => !!w)
 
   baseOpts.themeSet = themeFiles.map((w) => w.path)
-  baseOpts.vscode.themeFiles = themeFiles
+  baseOpts.vscode = { themeFiles }
 
   return baseOpts
 }


Test patch:
diff --git a/src/commands/export.test.ts b/src/commands/export.test.ts
index 8556ea0..dc933a4 100644
--- a/src/commands/export.test.ts
+++ b/src/commands/export.test.ts
@@ -1,3 +1,4 @@
+import * as marpCliModule from '@marp-team/marp-cli'
 import { commands, env, window, workspace } from 'vscode'
 import * as marpCli from '../marp-cli'
 import * as option from '../option'
@@ -317,6 +318,65 @@ describe('#doExport', () => {
     })
   })
 
+  describe('when CLI was thrown CLIError with BROWSER_NOT_FOUND error code', () => {
+    it.each`
+      browser      | platform    | expected
+      ${'auto'}    | ${'win32'}  | ${['Google Chrome', 'Microsoft Edge', 'Firefox']}
+      ${'auto'}    | ${'darwin'} | ${['Google Chrome', 'Microsoft Edge', 'Firefox']}
+      ${'auto'}    | ${'linux'}  | ${['Google Chrome', 'Chromium', 'Microsoft Edge', 'Firefox']}
+      ${'chrome'}  | ${'win32'}  | ${['Google Chrome']}
+      ${'chrome'}  | ${'darwin'} | ${['Google Chrome']}
+      ${'chrome'}  | ${'linux'}  | ${['Google Chrome', 'Chromium']}
+      ${'edge'}    | ${'win32'}  | ${['Microsoft Edge']}
+      ${'edge'}    | ${'darwin'} | ${['Microsoft Edge']}
+      ${'edge'}    | ${'linux'}  | ${['Microsoft Edge']}
+      ${'firefox'} | ${'win32'}  | ${['Firefox']}
+      ${'firefox'} | ${'darwin'} | ${['Firefox']}
+      ${'firefox'} | ${'linux'}  | ${['Firefox']}
+    `(
+      'throws MarpCLIError with the message contains $expected to suggest browsers when running on $platform with browser option as $browser',
+      async ({ browser, platform, expected }) => {
+        expect.assertions(expected.length + 1)
+        setConfiguration({ 'markdown.marp.browser': browser })
+
+        const { platform: originalPlatform } = process
+
+        try {
+          Object.defineProperty(process, 'platform', { value: platform })
+
+          const runMarpCLI = jest
+            .spyOn(marpCli, 'default')
+            .mockImplementation(async (_, __, opts) => {
+              opts?.onCLIError?.({
+                error: new marpCliModule.CLIError(
+                  'mocked error',
+                  marpCliModule.CLIErrorCode.NOT_FOUND_BROWSER,
+                ),
+                codes: marpCliModule.CLIErrorCode,
+              })
+            })
+
+          try {
+            await exportModule.doExport(saveURI(), document)
+            expect(window.showErrorMessage).toHaveBeenCalledTimes(1)
+
+            for (const fragment of expected) {
+              expect(window.showErrorMessage).toHaveBeenCalledWith(
+                expect.stringContaining(fragment),
+              )
+            }
+          } finally {
+            runMarpCLI.mockRestore()
+          }
+        } finally {
+          Object.defineProperty(process, 'platform', {
+            value: originalPlatform,
+          })
+        }
+      },
+    )
+  })
+
   describe('when the save path has non-file scheme', () => {
     it('exports the document into temporally path and copy it to the save path', async () => {
       const marpCliMock = jest.spyOn(marpCli, 'default').mockImplementation()
diff --git a/src/marp-cli.test.ts b/src/marp-cli.test.ts
index 80e0adc..0e710f4 100644
--- a/src/marp-cli.test.ts
+++ b/src/marp-cli.test.ts
@@ -7,10 +7,6 @@ import { textEncoder } from './utils'
 
 jest.mock('vscode')
 
-const setConfiguration: (conf?: Record<string, unknown>) => void = (
-  workspace as any
-)._setConfiguration
-
 describe('Marp CLI integration', () => {
   const runMarpCli = marpCli.default
 
@@ -31,7 +27,7 @@ describe('Marp CLI integration', () => {
   })
 
   it('runs Marp CLI with passed args', async () => {
-    const marpCliSpy = jest.spyOn(marpCliModule, 'marpCli')
+    const marpCliSpy = jest.spyOn(marpCliModule, 'marpCli').mockResolvedValue(0)
     await runMarpCli(['--version'])
 
     expect(marpCliSpy).toHaveBeenCalledWith(['--version'], undefined)
@@ -50,67 +46,6 @@ describe('Marp CLI integration', () => {
       marpCliMock.mockRestore()
     }
   })
-
-  it.each`
-    platform    | expected
-    ${'win32'}  | ${[/Google Chrome/, /Microsoft Edge/]}
-    ${'darwin'} | ${[/Google Chrome/, /Microsoft Edge/]}
-    ${'linux'}  | ${[/Google Chrome/, /Chromium/]}
-  `(
-    'contains $expected to suggested browsers in error message when running on $platform',
-    async ({ platform, expected }) => {
-      expect.assertions(expected.length)
-
-      const originalPlatform = process.platform
-
-      try {
-        Object.defineProperty(process, 'platform', { value: platform })
-
-        const marpCliMock = jest
-          .spyOn(marpCliModule, 'marpCli')
-          .mockRejectedValue(
-            new marpCliModule.CLIError(
-              'mocked error',
-              marpCliModule.CLIErrorCode.NOT_FOUND_CHROMIUM,
-            ),
-          )
-
-        try {
-          for (const fragment of expected) {
-            await expect(runMarpCli(['--version'])).rejects.toThrow(fragment)
-          }
-        } finally {
-          marpCliMock.mockRestore()
-        }
-      } finally {
-        Object.defineProperty(process, 'platform', { value: originalPlatform })
-      }
-    },
-  )
-
-  describe('with markdown.marp.chromePath preference', () => {
-    it('runs Marp CLI with overridden CHROME_PATH environment', async () => {
-      const { CHROME_PATH } = process.env
-      expect(process.env.CHROME_PATH).toBe(CHROME_PATH)
-
-      setConfiguration({ 'markdown.marp.chromePath': __filename })
-
-      const marpCliMock = jest
-        .spyOn(marpCliModule, 'marpCli')
-        .mockImplementation(async () => {
-          expect(process.env.CHROME_PATH).toBe(__filename)
-          return 0
-        })
-
-      try {
-        await runMarpCli(['--version'])
-        expect(marpCliMock).toHaveBeenCalled()
-        expect(process.env.CHROME_PATH).toBe(CHROME_PATH)
-      } finally {
-        marpCliMock.mockRestore()
-      }
-    })
-  })
 })
 
 describe('#createWorkFile', () => {
diff --git a/src/option.test.ts b/src/option.test.ts
index 6bb96cf..de44c2e 100644
--- a/src/option.test.ts
+++ b/src/option.test.ts
@@ -1,5 +1,5 @@
 import * as nodeFetch from 'node-fetch'
-import { Uri, workspace } from 'vscode'
+import { Uri, window, workspace } from 'vscode'
 import * as option from './option'
 import { textEncoder } from './utils'
 
@@ -110,6 +110,62 @@ describe('Option', () => {
       })
     })
 
+    it('sets correct browser option and browser path option', async () => {
+      setConfiguration({
+        'markdown.marp.browser': 'chrome',
+        'markdown.marp.browserPath': '',
+      })
+      expect(await subject({ uri: untitledUri })).toStrictEqual(
+        expect.objectContaining({
+          browser: 'chrome',
+          browserPath: undefined,
+        }),
+      )
+
+      // With browser path
+      setConfiguration({
+        'markdown.marp.browser': 'auto',
+        'markdown.marp.browserPath': '/path/to/browser',
+      })
+      expect(await subject({ uri: untitledUri })).toStrictEqual(
+        expect.objectContaining({
+          browser: 'auto',
+          browserPath: '/path/to/browser',
+        }),
+      )
+
+      // Firefox browser auto detection
+      setConfiguration({
+        'markdown.marp.browser': 'auto',
+        'markdown.marp.browserPath': '/path/to/firefox',
+      })
+      expect(await subject({ uri: untitledUri })).toStrictEqual(
+        expect.objectContaining({
+          browser: 'firefox',
+          browserPath: '/path/to/firefox',
+        }),
+      )
+
+      // [Legacy] markdown.marp.chromePath
+      setConfiguration({
+        'markdown.marp.browser': 'auto',
+        'markdown.marp.browserPath': '',
+        'markdown.marp.chromePath': '/path/to/browser',
+      })
+      expect(await subject({ uri: untitledUri })).toStrictEqual(
+        expect.objectContaining({
+          browser: 'chrome',
+          browserPath: '/path/to/browser',
+        }),
+      )
+      expect(window.showWarningMessage).toHaveBeenCalledWith(
+        expect.stringContaining(
+          'The setting "markdown.marp.chromePath" is deprecated',
+        ),
+        expect.anything(),
+      )
+    })
+
     describe('when targeted document belongs to workspace', () => {
       const css = '/* @theme test */'