/** * Integration tests for Playwright adapter step 17 checkout flow with confirmation callback. * Tests the pause-for-confirmation mechanism before clicking checkout button. */ import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest'; import { FixtureServer } from '../../../packages/infrastructure/adapters/automation/FixtureServer'; import { PlaywrightAutomationAdapter } from '../../../packages/infrastructure/adapters/automation/PlaywrightAutomationAdapter'; import { StepId } from '../../../packages/domain/value-objects/StepId'; import { CheckoutConfirmation } from '../../../packages/domain/value-objects/CheckoutConfirmation'; import { CheckoutPrice } from '../../../packages/domain/value-objects/CheckoutPrice'; import { CheckoutState } from '../../../packages/domain/value-objects/CheckoutState'; describe('Playwright Step 17 Checkout Flow with Confirmation', () => { let server: FixtureServer; let adapter: PlaywrightAutomationAdapter; let baseUrl: string; beforeAll(async () => { server = new FixtureServer(); const serverInfo = await server.start(); baseUrl = serverInfo.url; adapter = new PlaywrightAutomationAdapter({ headless: true, timeout: 5000, baseUrl, mode: 'mock', }); const connectResult = await adapter.connect(); expect(connectResult.success).toBe(true); }); afterAll(async () => { await adapter.disconnect(); await server.stop(); }); beforeEach(async () => { await adapter.navigateToPage(server.getFixtureUrl(17)); // Clear any previous callback adapter.setCheckoutConfirmationCallback(undefined); }); describe('Checkout Confirmation Callback Injection', () => { it('should accept and store checkout confirmation callback', () => { const mockCallback = vi.fn(); // Should not throw expect(() => { adapter.setCheckoutConfirmationCallback(mockCallback); }).not.toThrow(); }); it('should allow clearing the callback by passing undefined', () => { const mockCallback = vi.fn(); adapter.setCheckoutConfirmationCallback(mockCallback); // Should not throw when clearing expect(() => { adapter.setCheckoutConfirmationCallback(undefined); }).not.toThrow(); }); }); describe('Step 17 Execution with Confirmation Flow', () => { it('should extract checkout info before requesting confirmation', async () => { const mockCallback = vi.fn().mockResolvedValue( CheckoutConfirmation.create('confirmed') ); adapter.setCheckoutConfirmationCallback(mockCallback); const stepId = StepId.create(17); const result = await adapter.executeStep(stepId, {}); expect(result.success).toBe(true); expect(mockCallback).toHaveBeenCalledTimes(1); // Verify callback was called with price and state const callArgs = mockCallback.mock.calls[0]; expect(callArgs).toHaveLength(2); const [price, state] = callArgs; expect(price).toBeInstanceOf(CheckoutPrice); expect(state).toBeInstanceOf(CheckoutState); }); it('should show "Awaiting confirmation..." overlay before callback', async () => { const mockCallback = vi.fn().mockImplementation(async () => { // Check overlay message during callback execution const page = adapter.getPage()!; const overlayText = await page.locator('#gridpilot-action').textContent(); expect(overlayText).toContain('Awaiting confirmation'); return CheckoutConfirmation.create('confirmed'); }); adapter.setCheckoutConfirmationCallback(mockCallback); const stepId = StepId.create(17); await adapter.executeStep(stepId, {}); expect(mockCallback).toHaveBeenCalled(); }); it('should click checkout button only if confirmation is "confirmed"', async () => { const mockCallback = vi.fn().mockResolvedValue( CheckoutConfirmation.create('confirmed') ); adapter.setCheckoutConfirmationCallback(mockCallback); const stepId = StepId.create(17); const result = await adapter.executeStep(stepId, {}); expect(result.success).toBe(true); // Verify button was clicked by checking if navigation occurred const page = adapter.getPage()!; const currentUrl = page.url(); // In mock mode, clicking checkout would navigate to a success page or different step expect(currentUrl).toBeDefined(); }); it('should NOT click checkout button if confirmation is "cancelled"', async () => { const mockCallback = vi.fn().mockResolvedValue( CheckoutConfirmation.create('cancelled') ); adapter.setCheckoutConfirmationCallback(mockCallback); const stepId = StepId.create(17); const result = await adapter.executeStep(stepId, {}); expect(result.success).toBe(false); expect(result.error).toContain('cancelled'); expect(mockCallback).toHaveBeenCalled(); }); it('should NOT click checkout button if confirmation is "timeout"', async () => { const mockCallback = vi.fn().mockResolvedValue( CheckoutConfirmation.create('timeout') ); adapter.setCheckoutConfirmationCallback(mockCallback); const stepId = StepId.create(17); const result = await adapter.executeStep(stepId, {}); expect(result.success).toBe(false); expect(result.error).toContain('timeout'); expect(mockCallback).toHaveBeenCalled(); }); it('should show success overlay after confirmed checkout', async () => { const mockCallback = vi.fn().mockResolvedValue( CheckoutConfirmation.create('confirmed') ); adapter.setCheckoutConfirmationCallback(mockCallback); const stepId = StepId.create(17); await adapter.executeStep(stepId, {}); // Check for success overlay const page = adapter.getPage()!; const overlayExists = await page.locator('#gridpilot-overlay').count(); expect(overlayExists).toBeGreaterThan(0); }); it('should execute step normally if no callback is set', async () => { // No callback set - should execute without confirmation const stepId = StepId.create(17); const result = await adapter.executeStep(stepId, {}); // Should succeed without asking for confirmation expect(result.success).toBe(true); }); it('should handle callback errors gracefully', async () => { const mockCallback = vi.fn().mockRejectedValue( new Error('Callback failed') ); adapter.setCheckoutConfirmationCallback(mockCallback); const stepId = StepId.create(17); const result = await adapter.executeStep(stepId, {}); expect(result.success).toBe(false); expect(result.error).toBeDefined(); expect(mockCallback).toHaveBeenCalled(); }); it('should pass correct price from CheckoutPriceExtractor to callback', async () => { let capturedPrice: CheckoutPrice | null = null; const mockCallback = vi.fn().mockImplementation(async (price: CheckoutPrice) => { capturedPrice = price; return CheckoutConfirmation.create('confirmed'); }); adapter.setCheckoutConfirmationCallback(mockCallback); const stepId = StepId.create(17); await adapter.executeStep(stepId, {}); expect(capturedPrice).not.toBeNull(); expect(capturedPrice).toBeInstanceOf(CheckoutPrice); // The mock fixture should have a price formatted as $X.XX expect(capturedPrice!.toDisplayString()).toMatch(/^\$\d+\.\d{2}$/); }); it('should pass correct state from CheckoutState validation to callback', async () => { let capturedState: CheckoutState | null = null; const mockCallback = vi.fn().mockImplementation( async (_price: CheckoutPrice, state: CheckoutState) => { capturedState = state; return CheckoutConfirmation.create('confirmed'); } ); adapter.setCheckoutConfirmationCallback(mockCallback); const stepId = StepId.create(17); await adapter.executeStep(stepId, {}); expect(capturedState).not.toBeNull(); expect(capturedState).toBeInstanceOf(CheckoutState); // State should indicate whether checkout is ready (method, not property) expect(typeof capturedState!.isReady()).toBe('boolean'); }); }); describe('Step 17 with Track State Configuration', () => { it('should set track state before requesting confirmation', async () => { const mockCallback = vi.fn().mockResolvedValue( CheckoutConfirmation.create('confirmed') ); adapter.setCheckoutConfirmationCallback(mockCallback); const stepId = StepId.create(17); const result = await adapter.executeStep(stepId, { trackState: 'moderately-low', }); expect(result.success).toBe(true); expect(mockCallback).toHaveBeenCalled(); }); }); });