1+ import { spawn , ChildProcess } from 'child_process' ;
2+ import { writeFile } from 'fs/promises' ;
3+ import path from 'path' ;
4+
5+ let serverProcess : ChildProcess ;
6+ let serverLogs : string [ ] = [ ] ;
7+ let logFilePath : string ;
8+
9+ export default async function globalSetup ( ) {
10+ const testEmail = 'test@playwright.local' ;
11+ const testPassword = 'test123456' ;
12+
13+ // Setup log file
14+ logFilePath = path . join ( process . cwd ( ) , 'playwright-server.log' ) ;
15+ console . log ( `Server logs will be written to: ${ logFilePath } ` ) ;
16+
17+ try {
18+ // Step 1: Check if server is already running (optimization)
19+ const serverAlreadyRunning = await isServerRunning ( ) ;
20+ if ( serverAlreadyRunning ) {
21+ console . log ( 'Server already running - using existing server for faster testing' ) ;
22+ // Skip server startup entirely
23+ } else {
24+ // Step 2: Start server with log capture
25+ console . log ( 'Starting server...' ) ;
26+ serverProcess = await startServerWithLogCapture ( ) ;
27+
28+ // Step 3: Wait for server to be ready
29+ await waitForServerReady ( ) ;
30+ console . log ( 'Server is ready' ) ;
31+ }
32+
33+ // Step 4: Try existing test user first
34+ const loginSuccess = await tryLogin ( testEmail , testPassword ) ;
35+
36+ if ( loginSuccess ) {
37+ console . log ( 'Using existing test user' ) ;
38+ return ;
39+ }
40+
41+ // Only try to register if we started the server (so we can capture logs)
42+ if ( ! serverAlreadyRunning && serverProcess ) {
43+ // Step 5: Register new test user
44+ console . log ( 'Creating new test user...' ) ;
45+ await registerUser ( testEmail , testPassword ) ;
46+
47+ // Step 6: Extract activation link from logs
48+ const activationUrl = await extractActivationLinkFromLogs ( ) ;
49+ console . log ( 'Found activation link' ) ;
50+
51+ // Step 7: Activate user
52+ await activateUser ( activationUrl ) ;
53+
54+ // Step 8: Verify login works - but don't fail if it doesn't
55+ const finalLoginSuccess = await tryLogin ( testEmail , testPassword ) ;
56+ if ( ! finalLoginSuccess ) {
57+ console . log ( 'Warning: Test user may not be fully activated, but proceeding with tests' ) ;
58+ console . log ( 'Tests may fail if authentication is required' ) ;
59+ // Don't throw error - let tests run and fail gracefully if needed
60+ } else {
61+ console . log ( 'Test user ready' ) ;
62+ }
63+ } else {
64+ console . log ( 'Server already running - assuming test user exists or tests will handle auth failures gracefully' ) ;
65+ }
66+
67+ } catch ( error ) {
68+ console . error ( 'Global setup failed:' , error ) ;
69+ console . log ( '\n=== SERVER LOGS ===' ) ;
70+ console . log ( serverLogs . join ( '' ) ) ;
71+ console . log ( '=== END LOGS ===\n' ) ;
72+ throw error ;
73+ }
74+ }
75+
76+ async function ensureNoServerRunning ( ) {
77+ const isRunning = await isServerRunning ( ) ;
78+ if ( isRunning ) {
79+ throw new Error ( 'Server already running on port 3000. Please stop it before running tests.\nRun: pkill -f procuretoy && pkill -f vite' ) ;
80+ }
81+ }
82+
83+ async function isServerRunning ( ) : Promise < boolean > {
84+ try {
85+ const response = await fetch ( 'http://localhost:3000' , {
86+ signal : AbortSignal . timeout ( 1000 )
87+ } ) ;
88+ return true ;
89+ } catch {
90+ return false ;
91+ }
92+ }
93+
94+ function startServerWithLogCapture ( ) : Promise < ChildProcess > {
95+ return new Promise ( ( resolve , reject ) => {
96+ // Use relative path approach
97+ const serverProcess = spawn ( 'cargo' , [ 'fullstack' ] , {
98+ cwd : process . cwd ( ) , // Should be procuretoy directory
99+ stdio : [ 'pipe' , 'pipe' , 'pipe' ] ,
100+ env : { ...process . env }
101+ } ) ;
102+
103+ // Capture all logs
104+ serverProcess . stdout ?. on ( 'data' , ( data : Buffer ) => {
105+ const logLine = data . toString ( ) ;
106+ serverLogs . push ( logLine ) ;
107+ writeLogToFile ( logLine ) ;
108+ } ) ;
109+
110+ serverProcess . stderr ?. on ( 'data' , ( data : Buffer ) => {
111+ const logLine = data . toString ( ) ;
112+ serverLogs . push ( `[STDERR] ${ logLine } ` ) ;
113+ writeLogToFile ( `[STDERR] ${ logLine } ` ) ;
114+ } ) ;
115+
116+ serverProcess . on ( 'error' , ( error ) => {
117+ console . error ( 'Server process error:' , error ) ;
118+ reject ( new Error ( `Failed to start server: ${ error . message } ` ) ) ;
119+ } ) ;
120+
121+ serverProcess . on ( 'exit' , ( code ) => {
122+ if ( code !== 0 ) {
123+ reject ( new Error ( `Server exited with code ${ code } ` ) ) ;
124+ }
125+ } ) ;
126+
127+ // Give server time to start, then resolve
128+ setTimeout ( ( ) => {
129+ if ( serverProcess . killed ) {
130+ reject ( new Error ( 'Server process was killed during startup' ) ) ;
131+ } else {
132+ resolve ( serverProcess ) ;
133+ }
134+ } , 2000 ) ;
135+ } ) ;
136+ }
137+
138+ async function writeLogToFile ( logLine : string ) {
139+ try {
140+ await writeFile ( logFilePath , logLine , { flag : 'a' } ) ;
141+ } catch {
142+ // Ignore file write errors
143+ }
144+ }
145+
146+ async function waitForServerReady ( ) : Promise < void > {
147+ const maxAttempts = 30 ; // 30 seconds
148+
149+ for ( let i = 0 ; i < maxAttempts ; i ++ ) {
150+ try {
151+ const response = await fetch ( 'http://localhost:3000' ) ;
152+ if ( response . ok ) return ; // Server is ready
153+ } catch {
154+ // Server not ready yet
155+ }
156+
157+ await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
158+ }
159+
160+ throw new Error ( 'Server did not become ready within 30 seconds' ) ;
161+ }
162+
163+ async function tryLogin ( email : string , password : string ) : Promise < boolean > {
164+ try {
165+ const response = await fetch ( 'http://localhost:3000/api/auth/login' , {
166+ method : 'POST' ,
167+ headers : { 'Content-Type' : 'application/json' } ,
168+ body : JSON . stringify ( { email, password } )
169+ } ) ;
170+ return response . ok ;
171+ } catch {
172+ return false ;
173+ }
174+ }
175+
176+ async function registerUser ( email : string , password : string ) : Promise < void > {
177+ const response = await fetch ( 'http://localhost:3000/api/auth/register' , {
178+ method : 'POST' ,
179+ headers : { 'Content-Type' : 'application/json' } ,
180+ body : JSON . stringify ( { email, password } )
181+ } ) ;
182+
183+ if ( ! response . ok ) {
184+ throw new Error ( `Registration failed: ${ response . status } ${ response . statusText } ` ) ;
185+ }
186+ }
187+
188+ async function extractActivationLinkFromLogs ( ) : Promise < string > {
189+ return new Promise ( ( resolve , reject ) => {
190+ const timeout = setTimeout ( ( ) =>
191+ reject ( new Error ( 'Activation link not found in logs within 15 seconds' ) ) , 15000 ) ;
192+
193+ // Check existing logs first
194+ for ( const logLine of serverLogs ) {
195+ const match = logLine . match ( / h t t p : \/ \/ l o c a l h o s t : 3 0 0 0 \/ a c t i v a t e \? t o k e n = [ ^ \s ] + / ) ;
196+ if ( match ) {
197+ clearTimeout ( timeout ) ;
198+ return resolve ( match [ 0 ] ) ;
199+ }
200+ }
201+
202+ // Listen for new logs
203+ const originalLength = serverLogs . length ;
204+ const checkNewLogs = ( ) => {
205+ for ( let i = originalLength ; i < serverLogs . length ; i ++ ) {
206+ const match = serverLogs [ i ] . match ( / h t t p : \/ \/ l o c a l h o s t : 3 0 0 0 \/ a c t i v a t e \? t o k e n = [ ^ \s ] + / ) ;
207+ if ( match ) {
208+ clearTimeout ( timeout ) ;
209+ return resolve ( match [ 0 ] ) ;
210+ }
211+ }
212+ setTimeout ( checkNewLogs , 100 ) ;
213+ } ;
214+
215+ checkNewLogs ( ) ;
216+ } ) ;
217+ }
218+
219+ async function activateUser ( activationUrl : string ) : Promise < void > {
220+ // Visit the activation URL directly (like a user clicking the email link)
221+ const response = await fetch ( activationUrl , {
222+ method : 'GET' ,
223+ redirect : 'follow'
224+ } ) ;
225+
226+ if ( ! response . ok ) {
227+ throw new Error ( `Activation failed: ${ response . status } ${ response . statusText } ` ) ;
228+ }
229+
230+ // Give the activation time to process in the database
231+ await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
232+ }
0 commit comments