import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { Page, BrowserContext } from 'playwright'; import { PlaywrightAuthSessionService } from '../../../../core/automation/infrastructure/adapters/automation/auth/PlaywrightAuthSessionService'; import type { PlaywrightBrowserSession } from '../../../../core/automation/infrastructure/adapters/automation/core/PlaywrightBrowserSession'; import type { SessionCookieStore } from '../../../../core/automation/infrastructure/adapters/automation/auth/SessionCookieStore'; import type { IPlaywrightAuthFlow } from '../../../../core/automation/infrastructure/adapters/automation/auth/PlaywrightAuthFlow'; import type { LoggerPort as Logger } from '../../../../core/automation/application/ports/LoggerPort'; import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState'; import { Result } from '../../../../core/shared/result/Result'; describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', () => { const originalEnv = { ...process.env }; let mockBrowserSession: PlaywrightBrowserSession; let mockCookieStore: SessionCookieStore; let mockAuthFlow: IPlaywrightAuthFlow; let mockLogger: Logger; let mockPage: Page; beforeEach(() => { process.env = { ...originalEnv }; mockLogger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn(), child: vi.fn(), flush: vi.fn().mockResolvedValue(undefined), }; mockPage = { goto: vi.fn().mockResolvedValue(undefined), url: vi.fn().mockReturnValue('https://members-ng.iracing.com/web/racing/hosted/browse-sessions'), isClosed: vi.fn().mockReturnValue(false), } as unknown as Page; mockBrowserSession = { connect: vi.fn().mockResolvedValue({ success: true }), disconnect: vi.fn().mockResolvedValue(undefined), getPersistentContext: vi.fn().mockReturnValue(null as unknown as BrowserContext | null), getContext: vi.fn().mockReturnValue(null as unknown as BrowserContext | null), getPage: vi.fn().mockReturnValue(mockPage), getUserDataDir: vi.fn().mockReturnValue(''), } as unknown as PlaywrightBrowserSession; mockCookieStore = { read: vi.fn().mockResolvedValue({ cookies: [], origins: [], }), write: vi.fn().mockResolvedValue(undefined), delete: vi.fn().mockResolvedValue(undefined), validateCookies: vi.fn().mockReturnValue(AuthenticationState.UNKNOWN), getSessionExpiry: vi.fn(), getValidCookiesForUrl: vi.fn().mockReturnValue([]), } as unknown as SessionCookieStore; mockAuthFlow = { getLoginUrl: vi.fn().mockReturnValue('https://members-ng.iracing.com/login'), getPostLoginLandingUrl: vi.fn().mockReturnValue('https://members-ng.iracing.com/web/racing/hosted/browse-sessions'), isLoginUrl: vi.fn().mockReturnValue(false), isAuthenticatedUrl: vi.fn().mockReturnValue(true), isLoginSuccessUrl: vi.fn().mockReturnValue(true), detectAuthenticatedUi: vi.fn().mockResolvedValue(true), detectLoginUi: vi.fn().mockResolvedValue(false), navigateToAuthenticatedArea: vi.fn().mockResolvedValue(undefined), waitForPostLoginRedirect: vi.fn().mockResolvedValue(true), } as unknown as IPlaywrightAuthFlow; }); afterEach(() => { process.env = { ...originalEnv }; vi.restoreAllMocks(); }); function createService() { return new PlaywrightAuthSessionService( mockBrowserSession, mockCookieStore, mockAuthFlow, mockLogger, { navigationTimeoutMs: 1000, loginWaitTimeoutMs: 1000, }, ); } it('always forces headed browser for login regardless of browser mode configuration', async () => { const service = createService(); const result = await service.initiateLogin(); expect(result.isOk()).toBe(true); expect(mockBrowserSession.connect).toHaveBeenCalledWith(true); }); it('navigates the headed page to the non-blank login URL', async () => { const service = createService(); const result = await service.initiateLogin(); expect(result.isOk()).toBe(true); expect(mockAuthFlow.getLoginUrl).toHaveBeenCalledTimes(1); expect(mockPage.goto).toHaveBeenCalledWith( 'https://members-ng.iracing.com/login', expect.objectContaining({ waitUntil: 'domcontentloaded', }), ); const calledUrl = (mockPage.goto as unknown as ReturnType).mock.calls[0]![0] as string; expect(calledUrl).not.toEqual('about:blank'); }); it('propagates connection failure from browserSession.connect', async () => { (mockBrowserSession.connect as unknown as ReturnType).mockResolvedValueOnce({ success: false, error: 'boom', }); const service = createService(); const result = await service.initiateLogin(); expect(result.isErr()).toBe(true); const err = result.unwrapErr(); expect(err).toBeInstanceOf(Error); expect(err.message).toContain('boom'); }); it('logs explicit headed login message for human companion flow', async () => { const service = createService(); const result = await service.initiateLogin(); expect(result.isOk()).toBe(true); expect(mockLogger.info).toHaveBeenCalledWith( 'Opening login in headed Playwright browser (forceHeaded=true)', expect.objectContaining({ forceHeaded: true }), ); }); });