import { describe, it, expect, beforeEach, vi } from 'vitest'; import { Result } from '../../../packages/shared/result/Result'; import { CheckoutPriceExtractor } from '../../../packages/infrastructure/adapters/automation/CheckoutPriceExtractor'; import { CheckoutStateEnum } from '../../../packages/domain/value-objects/CheckoutState'; /** * CheckoutPriceExtractor Integration Tests - GREEN PHASE * * Tests verify HTML parsing for checkout price extraction and state detection. */ type Page = ConstructorParameters[0]; type Locator = ReturnType; describe('CheckoutPriceExtractor Integration', () => { let mockPage: Page; let mockLocator: any; let mockPillLocator: any; beforeEach(() => { // Create nested locator mock for span.label-pill mockPillLocator = { textContent: vi.fn().mockResolvedValue('$0.50'), first: vi.fn().mockReturnThis(), locator: vi.fn().mockReturnThis(), }; mockLocator = { getAttribute: vi.fn(), innerHTML: vi.fn(), textContent: vi.fn(), locator: vi.fn(() => mockPillLocator), first: vi.fn().mockReturnThis(), }; mockPage = { locator: vi.fn((selector) => { if (selector === '.label-pill, .label-inverse') { return mockPillLocator; } return mockLocator; }), }; }); describe('Success state HTML extraction', () => { it('should extract $0.50 from success button', async () => { const buttonHtml = '$0.50'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$0.50'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); const info = result.unwrap(); expect(info.price).not.toBeNull(); expect(info.price!.getAmount()).toBe(0.50); expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); }); it('should extract $5.00 from success button', async () => { const buttonHtml = '$5.00'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$5.00'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); const info = result.unwrap(); expect(info.price!.getAmount()).toBe(5.00); expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); }); it('should extract $100.00 from success button', async () => { const buttonHtml = '$100.00'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$100.00'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); const info = result.unwrap(); expect(info.price!.getAmount()).toBe(100.00); expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); }); it('should detect READY state from btn-success class', async () => { const buttonHtml = '$0.50'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$0.50'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().state.getValue()).toBe(CheckoutStateEnum.READY); }); }); describe('Insufficient funds HTML detection', () => { it('should detect INSUFFICIENT_FUNDS when btn-success is missing', async () => { const buttonHtml = '$0.50'; mockLocator.getAttribute.mockResolvedValue('btn btn-default'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$0.50'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); const info = result.unwrap(); expect(info.price).not.toBeNull(); expect(info.price!.getAmount()).toBe(0.50); expect(info.state.getValue()).toBe(CheckoutStateEnum.INSUFFICIENT_FUNDS); }); it('should still extract price when funds are insufficient', async () => { const buttonHtml = '$10.00'; mockLocator.getAttribute.mockResolvedValue('btn btn-default'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$10.00'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); const info = result.unwrap(); expect(info.price!.getAmount()).toBe(10.00); expect(info.state.getValue()).toBe(CheckoutStateEnum.INSUFFICIENT_FUNDS); }); it('should detect btn-primary as insufficient funds', async () => { const buttonHtml = '$0.50'; mockLocator.getAttribute.mockResolvedValue('btn btn-primary'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$0.50'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().state.getValue()).toBe(CheckoutStateEnum.INSUFFICIENT_FUNDS); }); }); describe('Price parsing variations', () => { it('should parse price with nested span tags', async () => { const buttonHtml = '$0.50'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$0.50'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().price!.getAmount()).toBe(0.50); }); it('should parse price with whitespace', async () => { const buttonHtml = ' $0.50 '; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue(' $0.50 '); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().price!.getAmount()).toBe(0.50); }); it('should parse price with multiple classes', async () => { const buttonHtml = '$0.50'; mockLocator.getAttribute.mockResolvedValue('btn btn-lg btn-success pull-right'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$0.50'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().price!.getAmount()).toBe(0.50); expect(result.unwrap().state.getValue()).toBe(CheckoutStateEnum.READY); }); }); describe('Missing button handling', () => { it('should return UNKNOWN state when button not found', async () => { mockLocator.getAttribute.mockResolvedValue(null); mockLocator.innerHTML.mockRejectedValue(new Error('Element not found')); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); const info = result.unwrap(); expect(info.price).toBeNull(); expect(info.state.getValue()).toBe(CheckoutStateEnum.UNKNOWN); }); it('should return null price when button not found', async () => { mockLocator.getAttribute.mockResolvedValue(null); mockPillLocator.textContent.mockResolvedValue(null); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().price).toBeNull(); }); }); describe('Malformed HTML handling', () => { it('should return null price when price text is invalid', async () => { const buttonHtml = 'Invalid Price'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('Invalid Price'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); const info = result.unwrap(); expect(info.price).toBeNull(); expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); }); it('should return null price when price is missing dollar sign', async () => { const buttonHtml = '0.50'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('0.50'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().price).toBeNull(); }); it('should handle empty price text', async () => { const buttonHtml = ''; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue(''); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().price).toBeNull(); }); }); describe('Button HTML capture', () => { it('should capture full button HTML for debugging', async () => { const buttonHtml = '$0.50'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$0.50'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().buttonHtml).toBe(buttonHtml); }); it('should capture button HTML even when price parsing fails', async () => { const buttonHtml = 'Invalid'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('Invalid'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().buttonHtml).toBe(buttonHtml); }); it('should return empty buttonHtml when button not found', async () => { mockLocator.getAttribute.mockResolvedValue(null); mockLocator.innerHTML.mockResolvedValue(''); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().buttonHtml).toBe(''); }); }); describe('BDD Scenarios', () => { it('Given checkout button with $0.50 and btn-success, When extracting, Then price is $0.50 and state is READY', async () => { const buttonHtml = '$0.50'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$0.50'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); const info = result.unwrap(); expect(info.price!.getAmount()).toBe(0.50); expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); }); it('Given checkout button with $0.50 without btn-success, When extracting, Then state is INSUFFICIENT_FUNDS', async () => { const buttonHtml = '$0.50'; mockLocator.getAttribute.mockResolvedValue('btn btn-default'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('$0.50'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); expect(result.unwrap().state.getValue()).toBe(CheckoutStateEnum.INSUFFICIENT_FUNDS); }); it('Given button not found, When extracting, Then state is UNKNOWN and price is null', async () => { mockLocator.getAttribute.mockResolvedValue(null); mockLocator.innerHTML.mockResolvedValue(''); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); const info = result.unwrap(); expect(info.price).toBeNull(); expect(info.state.getValue()).toBe(CheckoutStateEnum.UNKNOWN); }); it('Given malformed price text, When extracting, Then price is null but state is detected', async () => { const buttonHtml = 'Invalid'; mockLocator.getAttribute.mockResolvedValue('btn btn-success'); mockLocator.innerHTML.mockResolvedValue(buttonHtml); mockPillLocator.textContent.mockResolvedValue('Invalid'); const extractor = new CheckoutPriceExtractor(mockPage); const result = await extractor.extractCheckoutInfo(); expect(result.isOk()).toBe(true); const info = result.unwrap(); expect(info.price).toBeNull(); expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); }); }); });