Skip to content

Commit 76980e2

Browse files
committed
fix(test-utils): Wait for app to be ready before resolving startSpotlight
- Pass PORT env var to Spotlight so app runs on expected port (3030) - Add appPort option to SpotlightOptions (defaults to 3030) - Poll app's HTTP endpoint to confirm it's ready before resolving - This fixes ERR_CONNECTION_REFUSED errors in tests
1 parent 86cce1f commit 76980e2

File tree

1 file changed

+45
-13
lines changed

1 file changed

+45
-13
lines changed

dev-packages/test-utils/src/spotlight.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { parseEnvelope } from '@sentry/core';
33
import type { ChildProcess } from 'child_process';
44
import { spawn } from 'child_process';
55
import * as fs from 'fs';
6+
import * as http from 'http';
67
import * as path from 'path';
78
import * as readline from 'readline';
89
import { fileURLToPath } from 'url';
@@ -49,10 +50,14 @@ const SPOTLIGHT_BIN = path.join(REPO_ROOT, 'node_modules', '.bin', 'spotlight');
4950
interface SpotlightOptions {
5051
/** Port for the Spotlight sidecar. Use 0 for dynamic port assignment. */
5152
port?: number;
53+
/** Port for the app to run on. Defaults to 3030. */
54+
appPort?: number;
5255
/** Working directory for the child process (where package.json is located) */
5356
cwd?: string;
5457
/** Whether to enable debug output */
5558
debug?: boolean;
59+
/** Additional environment variables to pass to the process */
60+
env?: Record<string, string>;
5661
}
5762

5863
interface SpotlightInstance {
@@ -87,7 +92,7 @@ const eventListeners: Set<(envelope: Envelope) => void> = new Set();
8792
* - Streams events in JSON format (with -f json flag)
8893
*/
8994
export async function startSpotlight(options: SpotlightOptions = {}): Promise<SpotlightInstance> {
90-
const { port = 0, cwd = process.cwd(), debug = false } = options;
95+
const { port = 0, appPort = 3030, cwd = process.cwd(), debug = false, env = {} } = options;
9196

9297
return new Promise((resolve, reject) => {
9398
// Run Spotlight directly from repo root's node_modules
@@ -104,6 +109,11 @@ export async function startSpotlight(options: SpotlightOptions = {}): Promise<Sp
104109
const spotlightProcess = spawn(SPOTLIGHT_BIN, args, {
105110
cwd,
106111
stdio: ['ignore', 'pipe', 'pipe'],
112+
env: {
113+
...process.env,
114+
...env,
115+
PORT: String(appPort),
116+
},
107117
});
108118

109119
let resolvedPort: number | null = null;
@@ -121,6 +131,30 @@ export async function startSpotlight(options: SpotlightOptions = {}): Promise<Sp
121131
crlfDelay: Infinity,
122132
});
123133

134+
// Helper to poll the app port until it's ready
135+
const waitForAppReady = async (): Promise<void> => {
136+
const maxAttempts = 60; // 30 seconds
137+
for (let i = 0; i < maxAttempts; i++) {
138+
try {
139+
await new Promise<void>((resolveCheck, rejectCheck) => {
140+
const req = http.get(`http://localhost:${appPort}/`, res => {
141+
res.resume();
142+
resolveCheck();
143+
});
144+
req.on('error', rejectCheck);
145+
req.setTimeout(500, () => {
146+
req.destroy();
147+
rejectCheck(new Error('timeout'));
148+
});
149+
});
150+
return; // App is ready
151+
} catch {
152+
await new Promise(r => setTimeout(r, 500));
153+
}
154+
}
155+
throw new Error(`App did not start on port ${appPort} within 30 seconds`);
156+
};
157+
124158
stderrReader.on('line', (line: string) => {
125159
if (debug) {
126160
// eslint-disable-next-line no-console
@@ -136,21 +170,19 @@ export async function startSpotlight(options: SpotlightOptions = {}): Promise<Sp
136170

137171
if (portMatch?.[1] && !resolvedPort) {
138172
resolvedPort = parseInt(portMatch[1], 10);
139-
// Resolve immediately when we have the port from a "listening" message
140-
if (!resolved && line.includes('listening')) {
141-
resolved = true;
142-
const instance = createSpotlightInstance(spotlightProcess, resolvedPort, debug);
143-
currentSpotlightInstance = instance;
144-
resolve(instance);
145-
}
146173
}
147174

148-
// Fallback: check for other ready messages if we have a port
149-
if (!resolved && resolvedPort && (line.includes('running') || line.includes('started'))) {
175+
// When Spotlight says it's listening, start waiting for the app
176+
if (!resolved && resolvedPort && line.includes('listening')) {
150177
resolved = true;
151-
const instance = createSpotlightInstance(spotlightProcess, resolvedPort, debug);
152-
currentSpotlightInstance = instance;
153-
resolve(instance);
178+
// Wait for app to be ready before resolving
179+
waitForAppReady()
180+
.then(() => {
181+
const instance = createSpotlightInstance(spotlightProcess, resolvedPort, debug);
182+
currentSpotlightInstance = instance;
183+
resolve(instance);
184+
})
185+
.catch(reject);
154186
}
155187
});
156188

0 commit comments

Comments
 (0)