Skip to content

Commit b3f5d29

Browse files
committed
test: add FeedbackModal and FeedbackCell test coverage
1 parent c41086c commit b3f5d29

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { render, fireEvent } from '@solidjs/testing-library';
3+
import FeedbackModal, { FEEDBACK_TAGS } from '../../src/components/FeedbackModal';
4+
5+
describe('FeedbackModal', () => {
6+
it('does not render when closed', () => {
7+
const { container } = render(() => (
8+
<FeedbackModal open={false} onClose={vi.fn()} onSubmit={vi.fn()} />
9+
));
10+
expect(container.querySelector('.modal-backdrop')).toBeNull();
11+
});
12+
13+
it('renders when open', () => {
14+
const { container } = render(() => (
15+
<FeedbackModal open={true} onClose={vi.fn()} onSubmit={vi.fn()} />
16+
));
17+
expect(container.querySelector('.modal-backdrop')).not.toBeNull();
18+
expect(container.querySelector('.modal')).not.toBeNull();
19+
});
20+
21+
it('renders all feedback tags', () => {
22+
const { container } = render(() => (
23+
<FeedbackModal open={true} onClose={vi.fn()} onSubmit={vi.fn()} />
24+
));
25+
const tags = container.querySelectorAll('.feedback-tag');
26+
expect(tags.length).toBe(FEEDBACK_TAGS.length);
27+
for (let i = 0; i < FEEDBACK_TAGS.length; i++) {
28+
expect(tags[i]!.textContent).toBe(FEEDBACK_TAGS[i]);
29+
}
30+
});
31+
32+
it('toggles tag selection on click', () => {
33+
const { container } = render(() => (
34+
<FeedbackModal open={true} onClose={vi.fn()} onSubmit={vi.fn()} />
35+
));
36+
const tag = container.querySelector('.feedback-tag') as HTMLElement;
37+
expect(tag.classList.contains('feedback-tag--selected')).toBe(false);
38+
fireEvent.click(tag);
39+
expect(tag.classList.contains('feedback-tag--selected')).toBe(true);
40+
fireEvent.click(tag);
41+
expect(tag.classList.contains('feedback-tag--selected')).toBe(false);
42+
});
43+
44+
it('calls onSubmit with selected tags and details', () => {
45+
const onSubmit = vi.fn();
46+
const { container } = render(() => (
47+
<FeedbackModal open={true} onClose={vi.fn()} onSubmit={onSubmit} />
48+
));
49+
const tags = container.querySelectorAll('.feedback-tag');
50+
fireEvent.click(tags[0]!);
51+
fireEvent.click(tags[2]!);
52+
53+
const textarea = container.querySelector('.feedback-modal__textarea') as HTMLTextAreaElement;
54+
fireEvent.input(textarea, { target: { value: 'test details' } });
55+
56+
const submitBtn = container.querySelector('.btn--primary') as HTMLElement;
57+
fireEvent.click(submitBtn);
58+
59+
expect(onSubmit).toHaveBeenCalledWith(
60+
[FEEDBACK_TAGS[0], FEEDBACK_TAGS[2]],
61+
'test details',
62+
);
63+
});
64+
65+
it('calls onClose when close button is clicked', () => {
66+
const onClose = vi.fn();
67+
const { container } = render(() => (
68+
<FeedbackModal open={true} onClose={onClose} onSubmit={vi.fn()} />
69+
));
70+
const closeBtn = container.querySelector('.modal__close') as HTMLElement;
71+
fireEvent.click(closeBtn);
72+
expect(onClose).toHaveBeenCalled();
73+
});
74+
75+
it('calls onClose when clicking backdrop', () => {
76+
const onClose = vi.fn();
77+
const { container } = render(() => (
78+
<FeedbackModal open={true} onClose={onClose} onSubmit={vi.fn()} />
79+
));
80+
const backdrop = container.querySelector('.modal-backdrop') as HTMLElement;
81+
fireEvent.click(backdrop);
82+
expect(onClose).toHaveBeenCalled();
83+
});
84+
85+
it('does not call onClose when clicking modal content', () => {
86+
const onClose = vi.fn();
87+
const { container } = render(() => (
88+
<FeedbackModal open={true} onClose={onClose} onSubmit={vi.fn()} />
89+
));
90+
const modal = container.querySelector('.modal') as HTMLElement;
91+
fireEvent.click(modal);
92+
expect(onClose).not.toHaveBeenCalled();
93+
});
94+
95+
it('calls onClose on Escape key', () => {
96+
const onClose = vi.fn();
97+
render(() => <FeedbackModal open={true} onClose={onClose} onSubmit={vi.fn()} />);
98+
fireEvent.keyDown(document, { key: 'Escape' });
99+
expect(onClose).toHaveBeenCalled();
100+
});
101+
102+
it('resets state after submit', () => {
103+
const onSubmit = vi.fn();
104+
const { container } = render(() => (
105+
<FeedbackModal open={true} onClose={vi.fn()} onSubmit={onSubmit} />
106+
));
107+
const tag = container.querySelector('.feedback-tag') as HTMLElement;
108+
fireEvent.click(tag);
109+
expect(tag.classList.contains('feedback-tag--selected')).toBe(true);
110+
111+
const submitBtn = container.querySelector('.btn--primary') as HTMLElement;
112+
fireEvent.click(submitBtn);
113+
114+
// After submit, tags should be deselected
115+
expect(tag.classList.contains('feedback-tag--selected')).toBe(false);
116+
});
117+
118+
it('renders textarea with placeholder', () => {
119+
const { container } = render(() => (
120+
<FeedbackModal open={true} onClose={vi.fn()} onSubmit={vi.fn()} />
121+
));
122+
const textarea = container.querySelector('.feedback-modal__textarea') as HTMLTextAreaElement;
123+
expect(textarea).not.toBeNull();
124+
expect(textarea.placeholder).toBe('Share details (optional)');
125+
});
126+
});

packages/frontend/tests/components/MessageTable.test.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,4 +785,135 @@ describe('MessageTable', () => {
785785
expect(headers[4]!.textContent).toContain('Status');
786786
});
787787
});
788+
789+
describe('feedback column', () => {
790+
it('renders feedback buttons', () => {
791+
const { container } = render(() => (
792+
<MessageTable
793+
items={[makeRow()]}
794+
columns={['feedback']}
795+
agentName="agent-1"
796+
customProviderName={noopProvider}
797+
onFeedbackLike={vi.fn()}
798+
onFeedbackDislike={vi.fn()}
799+
onFeedbackClear={vi.fn()}
800+
/>
801+
));
802+
const buttons = container.querySelectorAll('.feedback-btn');
803+
expect(buttons.length).toBe(2);
804+
});
805+
806+
it('calls onFeedbackLike when thumb up is clicked', () => {
807+
const handler = vi.fn();
808+
const { container } = render(() => (
809+
<MessageTable
810+
items={[makeRow({ id: 'msg-like-test' })]}
811+
columns={['feedback']}
812+
agentName="agent-1"
813+
customProviderName={noopProvider}
814+
onFeedbackLike={handler}
815+
onFeedbackDislike={vi.fn()}
816+
onFeedbackClear={vi.fn()}
817+
/>
818+
));
819+
const likeBtn = container.querySelectorAll('.feedback-btn')[0] as HTMLElement;
820+
fireEvent.click(likeBtn);
821+
expect(handler).toHaveBeenCalledWith('msg-like-test');
822+
});
823+
824+
it('calls onFeedbackDislike when thumb down is clicked', () => {
825+
const handler = vi.fn();
826+
const { container } = render(() => (
827+
<MessageTable
828+
items={[makeRow({ id: 'msg-dislike-test' })]}
829+
columns={['feedback']}
830+
agentName="agent-1"
831+
customProviderName={noopProvider}
832+
onFeedbackLike={vi.fn()}
833+
onFeedbackDislike={handler}
834+
onFeedbackClear={vi.fn()}
835+
/>
836+
));
837+
const dislikeBtn = container.querySelectorAll('.feedback-btn')[1] as HTMLElement;
838+
fireEvent.click(dislikeBtn);
839+
expect(handler).toHaveBeenCalledWith('msg-dislike-test');
840+
});
841+
842+
it('calls onFeedbackClear when active like is clicked again', () => {
843+
const handler = vi.fn();
844+
const { container } = render(() => (
845+
<MessageTable
846+
items={[makeRow({ id: 'msg-clear-test', feedback_rating: 'like' })]}
847+
columns={['feedback']}
848+
agentName="agent-1"
849+
customProviderName={noopProvider}
850+
onFeedbackLike={vi.fn()}
851+
onFeedbackDislike={vi.fn()}
852+
onFeedbackClear={handler}
853+
/>
854+
));
855+
const likeBtn = container.querySelector('.feedback-btn--active-like') as HTMLElement;
856+
expect(likeBtn).not.toBeNull();
857+
fireEvent.click(likeBtn);
858+
expect(handler).toHaveBeenCalledWith('msg-clear-test');
859+
});
860+
861+
it('calls onFeedbackClear when active dislike is clicked again', () => {
862+
const handler = vi.fn();
863+
const { container } = render(() => (
864+
<MessageTable
865+
items={[makeRow({ id: 'msg-clear-test', feedback_rating: 'dislike' })]}
866+
columns={['feedback']}
867+
agentName="agent-1"
868+
customProviderName={noopProvider}
869+
onFeedbackLike={vi.fn()}
870+
onFeedbackDislike={vi.fn()}
871+
onFeedbackClear={handler}
872+
/>
873+
));
874+
const dislikeBtn = container.querySelector('.feedback-btn--active-dislike') as HTMLElement;
875+
expect(dislikeBtn).not.toBeNull();
876+
fireEvent.click(dislikeBtn);
877+
expect(handler).toHaveBeenCalledWith('msg-clear-test');
878+
});
879+
880+
it('shows active-like class when feedback_rating is like', () => {
881+
const { container } = render(() => (
882+
<MessageTable
883+
items={[makeRow({ feedback_rating: 'like' })]}
884+
columns={['feedback']}
885+
agentName="agent-1"
886+
customProviderName={noopProvider}
887+
/>
888+
));
889+
expect(container.querySelector('.feedback-btn--active-like')).not.toBeNull();
890+
expect(container.querySelector('.feedback-btn--active-dislike')).toBeNull();
891+
});
892+
893+
it('shows active-dislike class when feedback_rating is dislike', () => {
894+
const { container } = render(() => (
895+
<MessageTable
896+
items={[makeRow({ feedback_rating: 'dislike' })]}
897+
columns={['feedback']}
898+
agentName="agent-1"
899+
customProviderName={noopProvider}
900+
/>
901+
));
902+
expect(container.querySelector('.feedback-btn--active-dislike')).not.toBeNull();
903+
expect(container.querySelector('.feedback-btn--active-like')).toBeNull();
904+
});
905+
906+
it('shows no active class when no feedback', () => {
907+
const { container } = render(() => (
908+
<MessageTable
909+
items={[makeRow()]}
910+
columns={['feedback']}
911+
agentName="agent-1"
912+
customProviderName={noopProvider}
913+
/>
914+
));
915+
expect(container.querySelector('.feedback-btn--active-like')).toBeNull();
916+
expect(container.querySelector('.feedback-btn--active-dislike')).toBeNull();
917+
});
918+
});
788919
});

0 commit comments

Comments
 (0)