Skip to content

Commit 10ca23a

Browse files
author
RobJellinghaus
committed
GraphQL user integration testing gets closer. Thanks Claude!
1 parent 7371e20 commit 10ca23a

8 files changed

Lines changed: 432 additions & 9 deletions

File tree

frontend/src/containers/LoginPage.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,17 @@ export const LoginPage = () => {
2626
<br />
2727
<div style={{ display: 'flex', flexFlow: 'column' }}>
2828
<label>Email</label>
29-
<input value={email} onChange={(e) => setEmail(e.target.value)} />
29+
<input
30+
name="email"
31+
type="email"
32+
value={email}
33+
onChange={(e) => setEmail(e.target.value)}
34+
/>
3035
</div>
3136
<div style={{ display: 'flex', flexFlow: 'column' }}>
3237
<label>Password</label>
3338
<input
39+
name="password"
3440
type="password"
3541
value={password}
3642
onChange={(e) => setPassword(e.target.value)}

globalSetup.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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(/http:\/\/localhost:3000\/activate\?token=[^\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(/http:\/\/localhost:3000\/activate\?token=[^\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+
}

globalTeardown.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { exec } from 'child_process';
2+
import { promisify } from 'util';
3+
import { unlink } from 'fs/promises';
4+
import path from 'path';
5+
6+
const execAsync = promisify(exec);
7+
8+
export default async function globalTeardown() {
9+
console.log('Cleaning up test environment...');
10+
11+
// Check if we should keep server running for faster iteration
12+
const keepServer = process.env.PLAYWRIGHT_KEEP_SERVER === 'true';
13+
14+
if (keepServer) {
15+
console.log('PLAYWRIGHT_KEEP_SERVER=true - leaving server running for faster iteration');
16+
return;
17+
}
18+
19+
try {
20+
// Kill all related processes with force
21+
await execAsync('pkill -f procuretoy || true');
22+
await execAsync('pkill -f vite || true');
23+
await execAsync('pkill -f fullstack || true');
24+
await execAsync('pkill -f node || true');
25+
26+
console.log('Server processes terminated');
27+
28+
// Clean up log file
29+
try {
30+
const logFilePath = path.join(process.cwd(), 'playwright-server.log');
31+
await unlink(logFilePath);
32+
} catch {
33+
// Log file cleanup is non-critical
34+
}
35+
36+
} catch (error) {
37+
console.log('Cleanup error (non-fatal):', error);
38+
}
39+
}

killall.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kill `pgrep procuretoy`
2+
kill `pgrep node`
3+
kill `pgrep vite`

playwright.config.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export default defineConfig({
2323
workers: process.env.CI ? 1 : undefined,
2424
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
2525
reporter: 'html',
26+
27+
/* Global setup and teardown */
28+
globalSetup: require.resolve('./globalSetup'),
29+
globalTeardown: require.resolve('./globalTeardown'),
30+
2631
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
2732
use: {
2833
/* Base URL to use in actions like `await page.goto('/')`. */
@@ -45,15 +50,16 @@ export default defineConfig({
4550
use: { ...devices['Desktop Chrome'] },
4651
},
4752

48-
{
49-
name: 'firefox',
50-
use: { ...devices['Desktop Firefox'] },
51-
},
53+
// Commented out for faster testing - uncomment when needed
54+
// {
55+
// name: 'firefox',
56+
// use: { ...devices['Desktop Firefox'] },
57+
// },
5258

53-
{
54-
name: 'webkit',
55-
use: { ...devices['Desktop Safari'] },
56-
},
59+
// {
60+
// name: 'webkit',
61+
// use: { ...devices['Desktop Safari'] },
62+
// },
5763

5864
/* Test against mobile viewports. */
5965
// {

testUtils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Page } from '@playwright/test';
2+
3+
export async function loginAsTestUser(page: Page) {
4+
await page.goto('http://localhost:3000/login');
5+
6+
// Wait for page to load
7+
await page.waitForLoadState('networkidle');
8+
9+
// Fill in login form using proper name attributes
10+
await page.fill('input[name="email"]', 'test@playwright.local');
11+
await page.fill('input[name="password"]', 'test123456');
12+
13+
// Click the login button
14+
await page.click('button:has-text("Login")');
15+
16+
try {
17+
// Wait for successful login redirect
18+
await page.waitForURL('**/', { timeout: 10000 });
19+
20+
// Verify we're actually logged in by checking for logout button
21+
await page.waitForSelector('text=Logout', { timeout: 5000 });
22+
23+
console.log('Successfully logged in as test user');
24+
} catch (error) {
25+
// If login fails, throw a descriptive error
26+
const currentUrl = page.url();
27+
throw new Error(`Login failed. Current URL: ${currentUrl}. The test user may not be properly activated.`);
28+
}
29+
}

0 commit comments

Comments
 (0)