diff --git a/e2e-tests/tests/react-router.test.ts b/e2e-tests/tests/react-router.test.ts index dcc13fec2..28ab169fc 100644 --- a/e2e-tests/tests/react-router.test.ts +++ b/e2e-tests/tests/react-router.test.ts @@ -2,7 +2,6 @@ import * as path from 'node:path'; import * as fs from 'node:fs'; import { Integration } from '../../lib/Constants'; import { - KEYS, TEST_ARGS, checkEnvBuildPlugin, checkFileContents, @@ -12,101 +11,92 @@ import { checkIfRunsOnProdMode, checkPackageJson, createIsolatedTestEnv, - startWizardInstance, + getWizardCommand, } from '../utils'; import { afterAll, beforeAll, describe, test, expect } from 'vitest'; +//@ts-expect-error - clifty is ESM only +import { KEYS, withEnv } from 'clifty'; + async function runWizardOnReactRouterProject( projectDir: string, - integration: Integration, opts?: { modifiedFiles?: boolean; }, -) { +): Promise { const { modifiedFiles = false } = opts || {}; - const wizardInstance = startWizardInstance(integration, projectDir); + const wizardInteraction = withEnv({ + cwd: projectDir, + }).defineInteraction(); if (modifiedFiles) { - await wizardInstance.waitForOutput('Do you want to continue anyway?'); - - await wizardInstance.sendStdinAndWaitForOutput( - [KEYS.ENTER], - 'Please select your package manager.', - ); - } else { - await wizardInstance.waitForOutput('Please select your package manager.'); + wizardInteraction + .whenAsked('Do you want to continue anyway?') + .respondWith(KEYS.ENTER); } - const tracingOptionPrompted = await wizardInstance.sendStdinAndWaitForOutput( - [KEYS.DOWN, KEYS.ENTER], - 'to track the performance of your application?', - { timeout: 240_000 }, - ); - - const replayOptionPrompted = - tracingOptionPrompted && - (await wizardInstance.sendStdinAndWaitForOutput( - [KEYS.ENTER], - 'to get a video-like reproduction of errors during a user session?', - )); - - const logOptionPrompted = - replayOptionPrompted && - (await wizardInstance.sendStdinAndWaitForOutput( - [KEYS.ENTER], - 'to send your application logs to Sentry?', - )); - - const profilingOptionPrompted = - logOptionPrompted && - (await wizardInstance.sendStdinAndWaitForOutput( - [KEYS.ENTER], - 'to track application performance in detail?', - )); - - const examplePagePrompted = - profilingOptionPrompted && - (await wizardInstance.sendStdinAndWaitForOutput( - [KEYS.ENTER], - 'Do you want to create an example page', - )); - - const mcpPrompted = - examplePagePrompted && - (await wizardInstance.sendStdinAndWaitForOutput( - [KEYS.ENTER], - 'Optionally add a project-scoped MCP server configuration for the Sentry MCP?', - { optional: true }, - )); + wizardInteraction + .whenAsked('Please select your package manager.') + .respondWith(KEYS.DOWN, KEYS.ENTER) + .expectOutput('Installing @sentry/react-router') + .expectOutput('Installed @sentry/react-router', { + timeout: 240_000, + }) + + .whenAsked('Do you want to enable Tracing') + .respondWith(KEYS.ENTER) + .whenAsked('Do you want to enable Session Replay') + .respondWith(KEYS.ENTER) + .whenAsked('Do you want to enable Logs') + .respondWith(KEYS.ENTER) + .whenAsked('Do you want to enable Profiling') + .respondWith(KEYS.ENTER) + .expectOutput('Installing @sentry/profiling-node') + .expectOutput('Installed @sentry/profiling-node', { + timeout: 240_000, + }) + .whenAsked('Do you want to create an example page') + .respondWith(KEYS.ENTER); - mcpPrompted && - (await wizardInstance.sendStdinAndWaitForOutput( - [KEYS.DOWN, KEYS.ENTER], - 'Successfully installed the Sentry React Router SDK!', - )); + if (modifiedFiles) { + wizardInteraction + .whenAsked('Would you like to try running npx react-router reveal') + .respondWith(KEYS.ENTER) + .whenAsked('Did you apply the snippet above?') + .respondWith(KEYS.ENTER); + } - wizardInstance.kill(); + return wizardInteraction + .whenAsked( + 'Optionally add a project-scoped MCP server configuration for the Sentry MCP?', + ) + .respondWith(KEYS.DOWN, KEYS.ENTER) + .expectOutput('Successfully installed the Sentry React Router SDK!') + .run(getWizardCommand(Integration.reactRouter)); } describe('React Router', () => { describe('with empty project', () => { - const integration = Integration.reactRouter; - + let wizardExitCode: number; const { projectDir, cleanup } = createIsolatedTestEnv( 'react-router-test-app', ); beforeAll(async () => { - await runWizardOnReactRouterProject(projectDir, integration); + wizardExitCode = await runWizardOnReactRouterProject(projectDir); }); afterAll(() => { cleanup(); }); + test('exits with exit code 0', () => { + expect(wizardExitCode).toBe(0); + }); + test('package.json is updated correctly', () => { - checkPackageJson(projectDir, integration); + checkPackageJson(projectDir, Integration.reactRouter); }); test('.env.sentry-build-plugin is created and contains the auth token', () => { @@ -212,7 +202,7 @@ describe('React Router', () => { describe('edge cases', () => { describe('existing Sentry setup', () => { - const integration = Integration.reactRouter; + let wizardExitCode: number; const { projectDir, cleanup } = createIsolatedTestEnv( 'react-router-test-app', @@ -245,7 +235,7 @@ startTransition(() => { });`; fs.writeFileSync(clientEntryPath, existingContent); - await runWizardOnReactRouterProject(projectDir, integration, { + wizardExitCode = await runWizardOnReactRouterProject(projectDir, { modifiedFiles: true, }); }); @@ -254,6 +244,10 @@ startTransition(() => { cleanup(); }); + test('exits with exit code 0', () => { + expect(wizardExitCode).toBe(0); + }); + test('wizard handles existing Sentry without duplication', () => { const clientContent = fs.readFileSync( `${projectDir}/app/entry.client.tsx`, @@ -273,7 +267,7 @@ startTransition(() => { // Only test the essential checks for this edge case test('package.json is updated correctly', () => { - checkPackageJson(projectDir, integration); + checkPackageJson(projectDir, Integration.reactRouter); }); test('essential files exist or wizard completes gracefully', () => { @@ -302,7 +296,7 @@ startTransition(() => { }); describe('missing entry files', () => { - const integration = Integration.reactRouter; + let wizardExitCode: number; const { projectDir, cleanup } = createIsolatedTestEnv( 'react-router-test-app', @@ -325,20 +319,24 @@ startTransition(() => { if (fs.existsSync(entryClientPath)) fs.unlinkSync(entryClientPath); if (fs.existsSync(entryServerPath)) fs.unlinkSync(entryServerPath); - await runWizardOnReactRouterProject(projectDir, integration); + wizardExitCode = await runWizardOnReactRouterProject(projectDir); }); afterAll(() => { cleanup(); }); + test('exits with exit code 0', () => { + expect(wizardExitCode).toBe(0); + }); + test('wizard creates missing entry files', () => { checkFileExists(`${projectDir}/app/entry.client.tsx`); checkFileExists(`${projectDir}/app/entry.server.tsx`); }); test('basic configuration still works', () => { - checkPackageJson(projectDir, integration); + checkPackageJson(projectDir, Integration.reactRouter); checkFileExists(`${projectDir}/instrument.server.mjs`); }); }); diff --git a/package.json b/package.json index 2afa3a290..9b1b4cd74 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "ts-node": "^10.9.1", "typescript": "^5.0.4", "vite": "^6.2.3", - "vitest": "^3.0.9" + "vitest": "3.0.9" }, "resolutions": { "simple-plist": "1.4.0-0", diff --git a/src/react-router/react-router-wizard.ts b/src/react-router/react-router-wizard.ts index d9a376d33..c79357663 100644 --- a/src/react-router/react-router-wizard.ts +++ b/src/react-router/react-router-wizard.ts @@ -155,7 +155,7 @@ async function runReactRouterWizardWithTelemetry( traceStep('Reveal missing entry files', () => { try { - runReactRouterReveal(typeScriptDetected); + runReactRouterReveal(); clack.log.success('Entry files are ready for instrumentation'); } catch (e) { clack.log.warn(`Could not run 'npx react-router reveal'. diff --git a/src/react-router/sdk-setup.ts b/src/react-router/sdk-setup.ts index 6de697651..493c95863 100644 --- a/src/react-router/sdk-setup.ts +++ b/src/react-router/sdk-setup.ts @@ -121,11 +121,10 @@ async function ensureEntryFileExists( } } -export function runReactRouterReveal(force = false): void { +export function runReactRouterReveal(): void { if ( - force || - (!fs.existsSync(path.join(process.cwd(), 'app/entry.client.tsx')) && - !fs.existsSync(path.join(process.cwd(), 'app/entry.client.jsx'))) + !fs.existsSync(path.join(process.cwd(), 'app', 'entry.client.tsx')) && + !fs.existsSync(path.join(process.cwd(), 'app', 'entry.client.jsx')) ) { try { childProcess.execSync(REACT_ROUTER_REVEAL_COMMAND, { diff --git a/test/react-router/sdk-setup.test.ts b/test/react-router/sdk-setup.test.ts index c35d19dc6..b55987900 100644 --- a/test/react-router/sdk-setup.test.ts +++ b/test/react-router/sdk-setup.test.ts @@ -210,7 +210,7 @@ describe('runReactRouterReveal', () => { (childProcess.execSync as unknown as Mock).mockImplementation(() => 'ok'); - runReactRouterReveal(false); + runReactRouterReveal(); expect(childProcess.execSync).toHaveBeenCalledWith( 'npx react-router reveal', @@ -226,7 +226,7 @@ describe('runReactRouterReveal', () => { (childProcess.execSync as unknown as Mock).mockReset(); - runReactRouterReveal(false); + runReactRouterReveal(); expect(childProcess.execSync).not.toHaveBeenCalled(); }); diff --git a/yarn.lock b/yarn.lock index 9e8706d07..5982d9102 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3531,9 +3531,9 @@ vite-node@3.0.9: optionalDependencies: fsevents "~2.3.3" -vitest@^3.0.9: +vitest@3.0.9: version "3.0.9" - resolved "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.0.9.tgz#8cf607d27dcaa12b9f21111f001a4e3e92511ba5" integrity sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ== dependencies: "@vitest/expect" "3.0.9"