diff --git a/src/index.ts b/src/index.ts index b962201..2b5e762 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { main, parseCli } from './cli.ts'; import { createContext } from './context.ts'; import { getEnvironment, isTelemetryDisabled } from './environment.ts'; import { welcomeBannerBody, welcomeBannerTitle } from './interactive/banner.ts'; -import { runOpenReportPrompt } from './interactive/openReportPrompt.ts'; +import { openReport } from './interactive/openReport.ts'; import { runSharePrompt } from './interactive/sharePrompt.ts'; import { formatInteractiveTokenError, interactiveResolveToken } from './interactive/tokenWalkthrough.ts'; import { enforceTty } from './interactive/ttyGate.ts'; @@ -89,7 +89,7 @@ const ctx = createContext({ const result = await main(ctx, argv); if (result.kind === 'completed') { - await runOpenReportPrompt({ context: ctx, htmlPath: result.run.paths.html }); + await openReport({ context: ctx, htmlPath: result.run.paths.html }); await runSharePrompt({ context: ctx, target: result.run.target, diff --git a/src/interactive/openReport.test.ts b/src/interactive/openReport.test.ts new file mode 100644 index 0000000..8d601d1 --- /dev/null +++ b/src/interactive/openReport.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from 'bun:test'; +import { createFakeContext } from '../testHelpers/createFakeContext.ts'; +import { openReport } from './openReport.ts'; + +describe('openReport', () => { + test('opens the report in the browser automatically', async () => { + const handle = createFakeContext(); + + await openReport({ context: handle.ctx, htmlPath: '/tmp/report.html' }); + + expect(handle.browserOpener.opened).toEqual(['/tmp/report.html']); + expect(handle.analytics.capturedEvents('report_browser_open')[0]?.properties).toMatchObject({ success: true }); + }); + + test('does not warn when the browser opens cleanly', async () => { + const handle = createFakeContext(); + + await openReport({ context: handle.ctx, htmlPath: '/tmp/report.html' }); + + expect(handle.prompter.warns).toHaveLength(0); + }); + + test('warns and points at the file when the browser fails to open', async () => { + const handle = createFakeContext(); + handle.browserOpener.fails({ kind: 'open-failed', message: 'no display' }); + + await openReport({ context: handle.ctx, htmlPath: '/tmp/report.html' }); + + expect(handle.prompter.warns[0]).toContain('no display'); + expect(handle.prompter.warns[0]).toContain('/tmp/report.html'); + expect(handle.analytics.capturedEvents('report_browser_open')[0]?.properties).toMatchObject({ success: false }); + }); +}); diff --git a/src/interactive/openReport.ts b/src/interactive/openReport.ts new file mode 100644 index 0000000..18048e7 --- /dev/null +++ b/src/interactive/openReport.ts @@ -0,0 +1,23 @@ +import { formatBrowserOpenError } from '../BrowserOpener.ts'; +import type { Context } from '../context.ts'; + +export interface OpenReportInputs { + readonly context: Context; + readonly htmlPath: string; +} + +/** + * Open the freshly generated report in the user's browser, before the share + * prompt so they can eyeball it before deciding whether to send it. A failed + * launch degrades to a warning that points at the file on disk; either way the + * share prompt that follows prints the path. Never aborts the run. + */ +export async function openReport(inputs: OpenReportInputs): Promise { + const { prompter, analytics, browserOpener } = inputs.context; + + const opened = await browserOpener.open(inputs.htmlPath); + analytics.capture('report_browser_open', { success: opened.isOk() }); + if (opened.isErr()) { + prompter.warn(`${formatBrowserOpenError(opened.error)} Open it yourself: ${inputs.htmlPath}`); + } +} diff --git a/src/interactive/openReportPrompt.test.ts b/src/interactive/openReportPrompt.test.ts deleted file mode 100644 index 2ef355e..0000000 --- a/src/interactive/openReportPrompt.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { createFakeContext } from '../testHelpers/createFakeContext.ts'; -import { runOpenReportPrompt } from './openReportPrompt.ts'; - -describe('runOpenReportPrompt', () => { - test('defaults the confirm to yes', async () => { - const handle = createFakeContext(); - handle.prompter.scriptConfirm(true); - - await runOpenReportPrompt({ context: handle.ctx, htmlPath: '/tmp/report.html' }); - - expect(handle.prompter.confirms[0]?.defaultValue).toBe(true); - }); - - test('opens the report in the browser when confirmed', async () => { - const handle = createFakeContext(); - handle.prompter.scriptConfirm(true); - - await runOpenReportPrompt({ context: handle.ctx, htmlPath: '/tmp/report.html' }); - - expect(handle.browserOpener.opened).toEqual(['/tmp/report.html']); - expect(handle.analytics.capturedEvents('report_open_choice')[0]?.properties).toMatchObject({ opened: true }); - }); - - test('does not open when declined', async () => { - const handle = createFakeContext(); - handle.prompter.scriptConfirm(false); - - await runOpenReportPrompt({ context: handle.ctx, htmlPath: '/tmp/report.html' }); - - expect(handle.browserOpener.opened).toHaveLength(0); - expect(handle.analytics.capturedEvents('report_open_choice')[0]?.properties).toMatchObject({ opened: false }); - }); - - test('treats a cancelled prompt as no', async () => { - const handle = createFakeContext(); - handle.prompter.scriptConfirm({ kind: 'cancelled' }); - - await runOpenReportPrompt({ context: handle.ctx, htmlPath: '/tmp/report.html' }); - - expect(handle.browserOpener.opened).toHaveLength(0); - expect(handle.analytics.capturedEvents('report_open_choice')[0]?.properties).toMatchObject({ opened: false }); - }); - - test('warns and points at the file when the browser fails to open', async () => { - const handle = createFakeContext(); - handle.prompter.scriptConfirm(true); - handle.browserOpener.fails({ kind: 'open-failed', message: 'no display' }); - - await runOpenReportPrompt({ context: handle.ctx, htmlPath: '/tmp/report.html' }); - - expect(handle.prompter.warns[0]).toContain('no display'); - expect(handle.prompter.warns[0]).toContain('/tmp/report.html'); - }); -}); diff --git a/src/interactive/openReportPrompt.ts b/src/interactive/openReportPrompt.ts deleted file mode 100644 index e8c243f..0000000 --- a/src/interactive/openReportPrompt.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { formatBrowserOpenError } from '../BrowserOpener.ts'; -import type { Context } from '../context.ts'; - -export interface OpenReportPromptInputs { - readonly context: Context; - readonly htmlPath: string; -} - -/** - * Offer to open the freshly generated report in the user's browser. Defaults to - * "yes". Shown before the share prompt so they can eyeball the report before - * deciding whether to send it. A cancel or prompt error is treated as "no", and - * a failed launch degrades to pointing at the file on disk — neither aborts the run. - */ -export async function runOpenReportPrompt(inputs: OpenReportPromptInputs): Promise { - const { prompter, analytics, browserOpener } = inputs.context; - - const choice = await prompter.confirm({ - message: 'Open the report in your browser?', - defaultValue: true, - }); - - const shouldOpen = choice.unwrapOr(false); - analytics.capture('report_open_choice', { opened: shouldOpen }); - if (!shouldOpen) return; - - const opened = await browserOpener.open(inputs.htmlPath); - if (opened.isErr()) { - prompter.warn(`${formatBrowserOpenError(opened.error)} Open it yourself: ${inputs.htmlPath}`); - } -}