File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 11import type { Config } from "./types.js" ;
22import { executeCommand , type RunningProcess } from "./utils/execute-command.js" ;
3+ import { waitForUrl } from "./utils/wait-for-url.js" ;
34
45/**
56 * Start the application using lifecycle.start commands
67 * @param config The validated configuration object
8+ * @param url Optional URL to wait for after starting commands with keepAlive
79 * @returns Cleanup function that stops all running processes
810 */
9- export async function startApp ( config : Config ) : Promise < ( ) => Promise < void > > {
11+ export async function startApp ( config : Config , url ?: string ) : Promise < ( ) => Promise < void > > {
1012 const runningProcesses : RunningProcess [ ] = [ ] ;
1113
1214 try {
@@ -17,6 +19,14 @@ export async function startApp(config: Config): Promise<() => Promise<void>> {
1719 }
1820 }
1921
22+ // Wait for URL to become available if:
23+ // 1. A URL was provided AND
24+ // 2. At least one command has keepAlive: true
25+ const hasKeepAliveCommand = config . lifecycle . start . some ( ( cmd ) => cmd . keepAlive ) ;
26+ if ( url && hasKeepAliveCommand ) {
27+ await waitForUrl ( url ) ;
28+ }
29+
2030 // Return cleanup function
2131 return async ( ) => {
2232 for ( const { process, command } of runningProcesses ) {
Original file line number Diff line number Diff line change 1+ /**
2+ * Wait for a URL to become available by polling with fetch
3+ * @param url The URL to check (e.g., "http://localhost:3000")
4+ * @param timeout Maximum time to wait in milliseconds (default: 30000)
5+ * @throws Error if URL doesn't become available within timeout
6+ */
7+ export async function waitForUrl ( url : string , timeout : number = 30000 ) : Promise < void > {
8+ const startTime = Date . now ( ) ;
9+ const interval = 1000 ; // Poll every 1 second
10+
11+ while ( Date . now ( ) - startTime < timeout ) {
12+ try {
13+ // Use AbortSignal.timeout for per-request timeout (2 seconds)
14+ const response = await fetch ( url , { signal : AbortSignal . timeout ( 2000 ) } ) ;
15+
16+ // Consider 2xx-4xx as "available" - only 5xx errors mean server isn't ready
17+ // This handles cases where the app redirects or returns 404 before fully loading
18+ if ( response . ok || response . status < 500 ) {
19+ return ;
20+ }
21+ } catch ( _error ) {
22+ // Suppress errors during polling - connection errors are expected while server starts
23+ // Errors include: connection refused, timeout, DNS errors, etc.
24+ }
25+
26+ // Wait before next poll attempt
27+ await new Promise ( ( resolve ) => setTimeout ( resolve , interval ) ) ;
28+ }
29+
30+ // Timeout reached without successful connection
31+ throw new Error ( `Timeout: ${ url } did not become available within ${ timeout } ms` ) ;
32+ }
Original file line number Diff line number Diff line change @@ -21,7 +21,7 @@ export async function startRecording(storyId: string): Promise<void> {
2121 try {
2222 config = await loadConfig ( ) ;
2323 const story = await loadStory ( storyId ) ;
24- cleanup = await startApp ( config ) ;
24+ cleanup = await startApp ( config , story . start . url ) ;
2525 browserInstance = await launchBrowser ( story ) ;
2626 const { browser, page } = browserInstance ;
2727
Original file line number Diff line number Diff line change @@ -204,7 +204,7 @@ export async function runStory(
204204 const story = await loadStory ( storyId ) ;
205205
206206 // 3. Start application
207- cleanup = await startApp ( config ) ;
207+ cleanup = await startApp ( config , story . start . url ) ;
208208
209209 // 4. Launch browser
210210 browserInstance = await launchBrowser ( story ) ;
You can’t perform that action at this time.
0 commit comments