Skip to content

Commit e0c4ef9

Browse files
committed
feat(cli): add e2e testing support for login flow
- Add auto-login mechanism via CODEBUFF_E2E_NO_BROWSER flag - Add file-based IPC for login URL coordination (CODEBUFF_E2E_URL_FILE) - Add getWebsiteUrl() for dynamic URL resolution in tests - Support process.env override for CB_ENVIRONMENT in getConfigDir()
1 parent 16dd08f commit e0c4ef9

File tree

5 files changed

+110
-5
lines changed

5 files changed

+110
-5
lines changed

cli/src/components/login-modal.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,29 @@ export const LoginModal = ({
198198
}
199199
}, [hasOpenedBrowser, loginUrl, copyToClipboard])
200200

201+
// E2E auto-login: automatically trigger login URL fetch without waiting for Enter key
202+
// This is needed because OpenTUI keyboard events don't work reliably in PTY testing
203+
// The auto-login only activates when CODEBUFF_E2E_NO_BROWSER=true
204+
const hasTriggeredAutoLogin = useRef(false)
205+
206+
useEffect(() => {
207+
const isE2EMode = process.env.CODEBUFF_E2E_NO_BROWSER === 'true'
208+
209+
if (!isE2EMode) return
210+
if (hasTriggeredAutoLogin.current) return
211+
if (hasOpenedBrowser || loading) return
212+
213+
// Mark as triggered immediately to prevent double-triggering
214+
hasTriggeredAutoLogin.current = true
215+
216+
// Small delay to ensure component is fully mounted
217+
const timer = setTimeout(() => {
218+
fetchLoginUrlAndOpenBrowser()
219+
}, 1000)
220+
221+
return () => clearTimeout(timer)
222+
}, [hasOpenedBrowser, loading, fetchLoginUrlAndOpenBrowser])
223+
201224
// Calculate terminal width and height for responsive display
202225
const terminalWidth = renderer?.width || 80
203226
const terminalHeight = renderer?.height || 24

cli/src/hooks/use-fetch-login-url.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,41 @@
1+
import fs from 'fs'
2+
13
import { useMutation } from '@tanstack/react-query'
24
import open from 'open'
35

4-
import { WEBSITE_URL } from '../login/constants'
56
import { generateLoginUrl } from '../login/login-flow'
67
import { logger } from '../utils/logger'
8+
import { getWebsiteUrl } from '../login/constants'
9+
10+
/**
11+
* Check if we should skip browser opening for e2e tests.
12+
* When CODEBUFF_E2E_NO_BROWSER=true, we print the URL instead of opening browser.
13+
*/
14+
function shouldSkipBrowserOpen(): boolean {
15+
return process.env.CODEBUFF_E2E_NO_BROWSER === 'true'
16+
}
17+
18+
/**
19+
* Write login URL status to coordination file for e2e tests.
20+
* This provides reliable IPC between CLI and test runner.
21+
*/
22+
function writeE2ELoginStatus(status: 'pending' | 'ready' | 'error', data: { loginUrl?: string; error?: string }): void {
23+
const e2eUrlFile = process.env.CODEBUFF_E2E_URL_FILE
24+
if (!e2eUrlFile) return
25+
26+
try {
27+
const payload = {
28+
status,
29+
loginUrl: data.loginUrl,
30+
error: data.error,
31+
timestamp: Date.now(),
32+
}
33+
fs.writeFileSync(e2eUrlFile, JSON.stringify(payload, null, 2))
34+
} catch (err) {
35+
// Don't fail the login flow if we can't write the coordination file
36+
logger.debug({ err, e2eUrlFile }, 'Failed to write e2e login status file')
37+
}
38+
}
739

840
interface UseFetchLoginUrlParams {
941
setLoginUrl: (url: string | null) => void
@@ -27,12 +59,25 @@ export function useFetchLoginUrl({
2759
}: UseFetchLoginUrlParams) {
2860
const fetchLoginUrlMutation = useMutation({
2961
mutationFn: async (fingerprintId: string) => {
62+
// Get website URL dynamically to support e2e tests with custom server URLs
63+
const baseUrl = getWebsiteUrl()
64+
65+
// Debug logging for e2e tests
66+
if (process.env.CODEBUFF_E2E_NO_BROWSER === 'true') {
67+
process.stderr.write(`[E2E_FETCH] Starting mutation, baseUrl=${baseUrl}\n`)
68+
}
69+
70+
logger.debug({ baseUrl }, 'Fetching login URL')
71+
72+
// Write 'pending' status for e2e tests to confirm mutation was triggered
73+
writeE2ELoginStatus('pending', {})
74+
3075
return generateLoginUrl(
3176
{
3277
logger,
3378
},
3479
{
35-
baseUrl: WEBSITE_URL,
80+
baseUrl,
3681
fingerprintId,
3782
},
3883
)
@@ -44,6 +89,12 @@ export function useFetchLoginUrl({
4489
setIsWaitingForEnter(true)
4590
setHasOpenedBrowser(true)
4691

92+
// In e2e test mode, write URL to coordination file for reliable IPC
93+
if (shouldSkipBrowserOpen()) {
94+
writeE2ELoginStatus('ready', { loginUrl: data.loginUrl })
95+
return
96+
}
97+
4798
// Open browser after fetching URL
4899
try {
49100
await open(data.loginUrl)
@@ -53,7 +104,14 @@ export function useFetchLoginUrl({
53104
}
54105
},
55106
onError: (err) => {
56-
setError(err instanceof Error ? err.message : 'Failed to get login URL')
107+
const errorMessage = err instanceof Error ? err.message : 'Failed to get login URL'
108+
setError(errorMessage)
109+
110+
// In e2e test mode, write error to coordination file
111+
if (shouldSkipBrowserOpen()) {
112+
writeE2ELoginStatus('error', { error: errorMessage })
113+
}
114+
57115
logger.error(
58116
{
59117
error: err instanceof Error ? err.message : String(err),

cli/src/hooks/use-login-keyboard-handlers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ export function useLoginKeyboardHandlers({
2727
useKeyboard(
2828
useCallback(
2929
(key: KeyEvent) => {
30+
// Debug: log ALL key events in e2e mode
31+
if (process.env.CODEBUFF_E2E_NO_BROWSER === 'true') {
32+
process.stderr.write(`[E2E_KEY] Received key: ${key.name}, loading=${loading}, hasOpenedBrowser=${hasOpenedBrowser}\n`)
33+
}
34+
3035
const isEnter =
3136
(key.name === 'return' || key.name === 'enter') &&
3237
!key.ctrl &&

cli/src/login/constants.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
import { env } from '@codebuff/common/env'
22

33
// Get the website URL from environment or use default
4+
// This is the static version - prefer getWebsiteUrl() for dynamic access
45
export const WEBSITE_URL = env.NEXT_PUBLIC_CODEBUFF_APP_URL
56

7+
/**
8+
* Get website URL dynamically from process.env.
9+
* This is needed for e2e tests where the URL is set per-process
10+
* and the static WEBSITE_URL constant is evaluated at module load time.
11+
*/
12+
export function getWebsiteUrl(): string {
13+
// Check process.env first (for e2e tests with custom server)
14+
if (process.env.NEXT_PUBLIC_CODEBUFF_APP_URL) {
15+
return process.env.NEXT_PUBLIC_CODEBUFF_APP_URL
16+
}
17+
// Fall back to the statically parsed env
18+
return WEBSITE_URL
19+
}
20+
621
// Codebuff ASCII Logo - compact version for 80-width terminals
722
export const LOGO = `
823
██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██╗███████╗███████╗

cli/src/utils/auth.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ const credentialsSchema = z
3030

3131
// Get the config directory path
3232
export const getConfigDir = (): string => {
33+
// Use process.env directly for e2e tests where environment is set per-process
34+
// Fall back to parsed env for normal operation
35+
const cbEnvironment = process.env.NEXT_PUBLIC_CB_ENVIRONMENT || env.NEXT_PUBLIC_CB_ENVIRONMENT
36+
3337
return path.join(
3438
os.homedir(),
3539
'.config',
3640
'manicode' +
3741
// on a development stack?
38-
(env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod'
39-
? `-${env.NEXT_PUBLIC_CB_ENVIRONMENT}`
42+
(cbEnvironment !== 'prod'
43+
? `-${cbEnvironment}`
4044
: ''),
4145
)
4246
}

0 commit comments

Comments
 (0)