import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { ChooseNextResponseComponent } from './choose-next-response.component';
import { ChooseNextChallenge, ChooseNextOption } from '@ask-me-mcp/askme-shared';
describe('ChooseNextResponseComponent', () => {
let component: ChooseNextResponseComponent;
let fixture: ComponentFixture<ChooseNextResponseComponent>;
const mockChallenge: ChooseNextChallenge = {
id: 'challenge-1',
title: 'Choose Development Priority',
description: '# Next Development Phase\n\nWe need to decide which feature to develop next.\n\n## Context\nThe project is progressing well and we need to choose our **next focus area**.',
options: [
{
id: 'auth',
title: 'User Authentication',
description: 'Implement secure user login and registration system',
icon: '🔐'
},
{
id: 'dashboard',
title: 'Analytics Dashboard',
description: 'Create comprehensive data visualization and reporting tools',
icon: '📊'
},
{
id: 'api',
title: 'API Enhancement',
description: 'Improve performance and add new endpoints',
icon: '🚀'
}
]
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ChooseNextResponseComponent, FormsModule]
}).compileComponents();
fixture = TestBed.createComponent(ChooseNextResponseComponent);
component = fixture.componentInstance;
component.requestId = 'test-request-123';
component.challengeInput = mockChallenge;
fixture.detectChanges();
});
describe('Component Initialization', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display challenge title and description', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.challenge-title')?.textContent?.trim()).toBe('Choose Development Priority');
expect(compiled.querySelector('.challenge-description')).toBeTruthy();
});
it('should render all options as cards', () => {
const compiled = fixture.nativeElement as HTMLElement;
const optionCards = compiled.querySelectorAll('.option-card');
expect(optionCards.length).toBe(3);
const firstCard = optionCards[0];
expect(firstCard.querySelector('.option-title')?.textContent?.trim()).toBe('User Authentication');
expect(firstCard.querySelector('.option-description')?.textContent?.trim()).toBe('Implement secure user login and registration system');
expect(firstCard.querySelector('.option-icon')?.textContent?.trim()).toBe('🔐');
});
it('should show bail-out buttons', () => {
const compiled = fixture.nativeElement as HTMLElement;
const bailOutButtons = compiled.querySelectorAll('.big-bail-out-btn');
expect(bailOutButtons.length).toBe(3);
expect(bailOutButtons[0].textContent).toContain('Use Open Question Instead');
expect(bailOutButtons[1].textContent).toContain('Use Multiple Choice Instead');
expect(bailOutButtons[2].textContent).toContain('Use Hypothesis Challenge Instead');
});
});
describe('Option Selection (1-Click UX)', () => {
it('should immediately submit response when option is clicked', () => {
jest.spyOn(component.responseSubmitted, 'emit');
const compiled = fixture.nativeElement as HTMLElement;
const firstOption = compiled.querySelector('.option-card') as HTMLElement;
firstOption.click();
fixture.detectChanges();
expect(component.responseSubmitted.emit).toHaveBeenCalledWith({
requestId: 'test-request-123',
type: 'choose-next',
action: 'selected',
selectedOption: mockChallenge.options[0]
});
});
it('should show click indicator on hover', () => {
const compiled = fixture.nativeElement as HTMLElement;
const firstOption = compiled.querySelector('.option-card') as HTMLElement;
const clickIndicator = firstOption.querySelector('.click-indicator') as HTMLElement;
expect(clickIndicator).toBeTruthy();
expect(clickIndicator.textContent?.trim()).toBe('Click to Choose');
});
it('should not submit if already submitting', () => {
jest.spyOn(component.responseSubmitted, 'emit');
component['_isSubmitting'].set(true);
component.selectOption(mockChallenge.options[0]);
expect(component.responseSubmitted.emit).not.toHaveBeenCalled();
});
it('should set submitting state when option is selected', () => {
expect(component.isSubmitting()).toBe(false);
component.selectOption(mockChallenge.options[0]);
expect(component.isSubmitting()).toBe(true);
});
});
describe('Response Submission', () => {
it('should emit abort response when aborting', () => {
jest.spyOn(component.responseSubmitted, 'emit');
component['_additionalMessage'].set('I do not want any of these options');
component.submitAbort();
expect(component.responseSubmitted.emit).toHaveBeenCalledWith({
requestId: 'test-request-123',
type: 'choose-next',
action: 'abort',
message: 'I do not want any of these options'
});
});
it('should emit new ideas response when requesting new ideas', () => {
jest.spyOn(component.responseSubmitted, 'emit');
component['_additionalMessage'].set('Please provide different options');
component.submitNewIdeas();
expect(component.responseSubmitted.emit).toHaveBeenCalledWith({
requestId: 'test-request-123',
type: 'choose-next',
action: 'new-ideas',
message: 'Please provide different options'
});
});
it('should not submit abort if already submitting', () => {
jest.spyOn(component.responseSubmitted, 'emit');
component['_isSubmitting'].set(true);
component.submitAbort();
expect(component.responseSubmitted.emit).not.toHaveBeenCalled();
});
it('should not submit new ideas if already submitting', () => {
jest.spyOn(component.responseSubmitted, 'emit');
component['_isSubmitting'].set(true);
component.submitNewIdeas();
expect(component.responseSubmitted.emit).not.toHaveBeenCalled();
});
});
describe('Bail-out Functionality', () => {
it('should emit tool redirect for ask-one-question', () => {
jest.spyOn(component.toolRedirect, 'emit');
component.bailOutToTool('ask-one-question');
expect(component.toolRedirect.emit).toHaveBeenCalledWith({
requestId: 'test-request-123',
toolName: 'ask-one-question',
message: expect.stringContaining('STOP! User REFUSES the choose-next format and DEMANDS an open-ended question instead')
});
});
it('should emit tool redirect for ask-multiple-choice', () => {
jest.spyOn(component.toolRedirect, 'emit');
component.bailOutToTool('ask-multiple-choice');
expect(component.toolRedirect.emit).toHaveBeenCalledWith({
requestId: 'test-request-123',
toolName: 'ask-multiple-choice',
message: expect.stringContaining('STOP! User REFUSES the choose-next format and DEMANDS structured multiple choice questions instead')
});
});
it('should emit tool redirect for challenge-hypothesis', () => {
jest.spyOn(component.toolRedirect, 'emit');
component.bailOutToTool('challenge-hypothesis');
expect(component.toolRedirect.emit).toHaveBeenCalledWith({
requestId: 'test-request-123',
toolName: 'challenge-hypothesis',
message: expect.stringContaining('STOP! User REFUSES the choose-next format and DEMANDS hypothesis validation instead')
});
});
it('should not bail out if submitting', () => {
jest.spyOn(component.toolRedirect, 'emit');
component['_isSubmitting'].set(true);
component.bailOutToTool('ask-one-question');
expect(component.toolRedirect.emit).not.toHaveBeenCalled();
});
});
describe('UI Interactions', () => {
it('should handle keyboard navigation on option cards', () => {
jest.spyOn(component.responseSubmitted, 'emit');
const compiled = fixture.nativeElement as HTMLElement;
const firstOption = compiled.querySelector('.option-card') as HTMLElement;
// Test Enter key
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
firstOption.dispatchEvent(enterEvent);
fixture.detectChanges();
expect(component.responseSubmitted.emit).toHaveBeenCalledWith({
requestId: 'test-request-123',
type: 'choose-next',
action: 'selected',
selectedOption: mockChallenge.options[0]
});
// Reset for space key test
jest.clearAllMocks();
component['_isSubmitting'].set(false);
// Test Space key
const spaceEvent = new KeyboardEvent('keydown', { key: ' ' });
firstOption.dispatchEvent(spaceEvent);
fixture.detectChanges();
expect(component.responseSubmitted.emit).toHaveBeenCalledWith({
requestId: 'test-request-123',
type: 'choose-next',
action: 'selected',
selectedOption: mockChallenge.options[0]
});
});
it('should disable interactions when submitting', () => {
component['_isSubmitting'].set(true);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const optionCards = compiled.querySelectorAll('.option-card');
const bailOutButtons = compiled.querySelectorAll('.big-bail-out-btn');
const actionButtons = compiled.querySelectorAll('.action-btn');
optionCards.forEach(card => {
expect(card.classList.contains('disabled')).toBeTruthy();
expect(card.getAttribute('tabindex')).toBe('-1');
});
bailOutButtons.forEach(btn => {
expect((btn as HTMLButtonElement).disabled).toBeTruthy();
});
actionButtons.forEach(btn => {
expect((btn as HTMLButtonElement).disabled).toBeTruthy();
});
});
it('should handle additional message getter/setter', () => {
component.additionalMessage = 'Test message';
expect(component.additionalMessage).toBe('Test message');
expect(component['_additionalMessage']()).toBe('Test message');
});
});
describe('Markdown Formatting', () => {
it('should format markdown in description', () => {
expect(component.formattedDescription()).toContain('<h1>Next Development Phase</h1>');
expect(component.formattedDescription()).toContain('<strong>next focus area</strong>');
expect(component.formattedDescription()).toContain('<h2>Context</h2>');
});
it('should handle empty description', () => {
// Create a fresh component for this test
const freshFixture = TestBed.createComponent(ChooseNextResponseComponent);
const freshComponent = freshFixture.componentInstance;
const emptyDescChallenge: ChooseNextChallenge = {
id: 'test',
title: 'Test Challenge',
description: '',
options: [mockChallenge.options[0]]
};
freshComponent.requestId = 'test-request';
freshComponent.challengeInput = emptyDescChallenge;
freshFixture.detectChanges();
expect(freshComponent.challenge().description).toBe('');
expect(freshComponent.formattedDescription()).toBe('');
});
});
describe('Console Logging', () => {
it('should log selection events', () => {
jest.spyOn(console, 'log');
component.selectOption(mockChallenge.options[0]);
expect(console.log).toHaveBeenCalledWith('ChooseNext: Immediately submitting selection:', mockChallenge.options[0]);
});
it('should log abort events', () => {
jest.spyOn(console, 'log');
component.submitAbort();
expect(console.log).toHaveBeenCalledWith('ChooseNext: Submitting abort');
});
it('should log bail-out events', () => {
jest.spyOn(console, 'log');
component.bailOutToTool('ask-one-question');
expect(console.log).toHaveBeenCalledWith('ChooseNext: Bailing out to tool:', 'ask-one-question');
});
});
describe('Edge Cases', () => {
it('should handle challenge with no options', () => {
const freshFixture = TestBed.createComponent(ChooseNextResponseComponent);
const freshComponent = freshFixture.componentInstance;
const emptyChallenge: ChooseNextChallenge = {
id: 'empty',
title: 'Empty Challenge',
description: 'No options available',
options: []
};
freshComponent.requestId = 'test-request';
freshComponent.challengeInput = emptyChallenge;
freshFixture.detectChanges();
expect(freshComponent.challenge().options.length).toBe(0);
const compiled = freshFixture.nativeElement as HTMLElement;
const optionCards = compiled.querySelectorAll('.option-card');
expect(optionCards.length).toBe(0);
});
it('should handle challenge with no icons', () => {
const freshFixture = TestBed.createComponent(ChooseNextResponseComponent);
const freshComponent = freshFixture.componentInstance;
const noIconChallenge: ChooseNextChallenge = {
id: 'test',
title: 'Test Challenge',
description: 'Test description',
options: mockChallenge.options.map(opt => ({ ...opt, icon: undefined }))
};
freshComponent.requestId = 'test-request';
freshComponent.challengeInput = noIconChallenge;
freshFixture.detectChanges();
expect(freshComponent.challenge().options[0].icon).toBeUndefined();
const compiled = freshFixture.nativeElement as HTMLElement;
const icons = compiled.querySelectorAll('.option-icon');
expect(icons.length).toBe(0);
});
it('should handle null challenge input', () => {
const freshFixture = TestBed.createComponent(ChooseNextResponseComponent);
const freshComponent = freshFixture.componentInstance;
freshComponent.requestId = 'test-request';
freshComponent.challengeInput = null;
freshFixture.detectChanges();
const challenge = freshComponent.challenge();
expect(challenge.title).toBe('');
expect(challenge.options.length).toBe(0);
});
it('should trim whitespace from additional message in abort/new-ideas', () => {
component['_additionalMessage'].set(' test message ');
jest.spyOn(component.responseSubmitted, 'emit');
component.submitAbort();
expect(component.responseSubmitted.emit).toHaveBeenCalledWith(
expect.objectContaining({
message: 'test message'
})
);
});
it('should not include message field when empty in abort/new-ideas', () => {
component['_additionalMessage'].set(' ');
jest.spyOn(component.responseSubmitted, 'emit');
component.submitAbort();
expect(component.responseSubmitted.emit).toHaveBeenCalledWith(
expect.objectContaining({
message: undefined
})
);
});
});
});