378 lines
15 KiB
TypeScript
378 lines
15 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { Result } from '../../../packages/shared/result/Result';
|
|
import { CheckoutPriceExtractor } from '../../../packages/automation/infrastructure/adapters/automation/CheckoutPriceExtractor';
|
|
import { CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState';
|
|
|
|
/**
|
|
* CheckoutPriceExtractor Integration Tests - GREEN PHASE
|
|
*
|
|
* Tests verify HTML parsing for checkout price extraction and state detection.
|
|
*/
|
|
|
|
type Page = ConstructorParameters<typeof CheckoutPriceExtractor>[0];
|
|
type Locator = ReturnType<Page['locator']>;
|
|
|
|
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 = '<a class="btn btn-success"><span class="label label-pill label-inverse">$0.50</span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span class="label label-pill label-inverse">$5.00</span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span class="label label-pill label-inverse">$100.00</span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span>$0.50</span></a>';
|
|
|
|
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 = '<a class="btn btn-default"><span class="label label-pill label-inverse">$0.50</span></a>';
|
|
|
|
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 = '<a class="btn btn-default"><span>$10.00</span></a>';
|
|
|
|
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 = '<a class="btn btn-primary"><span>$0.50</span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span class="outer"><span class="inner">$0.50</span></span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span> $0.50 </span></a>';
|
|
|
|
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 = '<a class="btn btn-lg btn-success pull-right"><span class="label label-pill label-inverse">$0.50</span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span>Invalid Price</span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span>0.50</span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span></span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span class="label label-pill label-inverse">$0.50</span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span>Invalid</span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span>$0.50</span></a>';
|
|
|
|
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 = '<a class="btn btn-default"><span>$0.50</span></a>';
|
|
|
|
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 = '<a class="btn btn-success"><span>Invalid</span></a>';
|
|
|
|
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);
|
|
});
|
|
});
|
|
}); |