Skip to content

Commit 1873863

Browse files
jefischeshashilo
andauthored
fix #677: adding web socket for real-time gift generation updates (#678)
* fix #677: adding web socket for real-time gift generation updates * updating render logic for gift card components and adding unit tests * adding copilot feedback for duplicate gift check, unit test updates --------- Co-authored-by: Shashi Lo <362527+shashilo@users.noreply.github.com>
1 parent 338c333 commit 1873863

File tree

3 files changed

+140
-4
lines changed

3 files changed

+140
-4
lines changed

app/gift-exchanges/[id]/page.test.tsx

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,31 @@ import GiftExchangePage from './page';
33
import { useAuthContext } from '@/context/AuthContextProvider';
44
import * as utils from '@/lib/utils';
55
import { TOASTS } from '@/components/Toast/toasts.consts';
6+
import type { IGiftSuggestion } from '@/app/types/giftSuggestion';
67

78
const routerPush = jest.fn();
89

10+
jest.mock('@/lib/supabase/client', () => ({
11+
createClient: jest.fn(() => ({
12+
auth: {
13+
onAuthStateChange: jest.fn(),
14+
getSession: jest.fn().mockResolvedValue({ data: { session: null } }),
15+
getUser: jest.fn().mockResolvedValue({ data: { user: { id: 'test-member' } } }),
16+
},
17+
from: jest.fn(() => ({
18+
select: jest.fn().mockReturnThis(),
19+
eq: jest.fn().mockReturnThis(),
20+
single: jest.fn().mockReturnThis(),
21+
then: jest.fn(),
22+
})),
23+
channel: jest.fn(() => ({
24+
on: jest.fn().mockReturnThis(),
25+
subscribe: jest.fn().mockReturnThis(),
26+
})),
27+
removeChannel: jest.fn(),
28+
})),
29+
}));
30+
931
jest.mock('next/navigation', () => ({
1032
useParams: () => ({ id: '123' }),
1133
useRouter: () => ({
@@ -46,7 +68,49 @@ const mockGiftExchangeData = {
4668

4769
const mockGiftSuggestions = {
4870
match: null,
49-
suggestions: [],
71+
suggestions: [] as IGiftSuggestion[],
72+
};
73+
74+
const mockGiftSuggestionsFull = {
75+
match: null,
76+
suggestions: [
77+
{
78+
"price": "$50",
79+
"title": "Bamboo Cheese Board and Knife Set",
80+
"imageUrl": null,
81+
"matchScore": 80,
82+
"description": "This elegant bamboo cheese board comes with a hidden slide-out drawer that holds a cheese knife set. Perfect for hosting gatherings and enjoying a selection of cheeses.",
83+
"matchReasons": [
84+
"Luxurious bamboo material",
85+
"Ideal for hosting food-related events"
86+
],
87+
"id": "1451",
88+
},
89+
{
90+
"price": "$60",
91+
"title": "Swarovski Crystal Watch",
92+
"imageUrl": null,
93+
"matchScore": 90,
94+
"description": "A stunning Swarovski crystal watch that combines functionality with luxury. The intricate design and sparkling crystals make it a stylish accessory for any occasion.",
95+
"matchReasons": [
96+
"Luxurious item for a luxury watch enthusiast",
97+
"Adds a touch of style and fashion"
98+
],
99+
"id": "1454",
100+
},
101+
{
102+
"price": "$55",
103+
"title": "Artisanal Gluten-Free Baking Kit",
104+
"imageUrl": null,
105+
"matchScore": 85,
106+
"description": "A complete baking kit with high-quality gluten-free ingredients and recipes for creating delicious treats at home. Perfect for someone with a gluten allergy who enjoys baking.",
107+
"matchReasons": [
108+
"Caters to gluten allergy",
109+
"Combines food & cooking with arts & crafts"
110+
],
111+
"id": "1455",
112+
}
113+
] as IGiftSuggestion[],
50114
};
51115

52116
const mockGroupMember = {
@@ -74,7 +138,7 @@ const mockNonMemberSession = {
74138
const mockNoSession = { session: null };
75139

76140
describe('GiftExchangePage', () => {
77-
const mockDataFetch = (status: string) => {
141+
const mockDataFetch = (status: string, suggestions = mockGiftSuggestions) => {
78142
(global.fetch as jest.Mock)
79143
.mockResolvedValueOnce({
80144
json: async () => ({ ...mockGiftExchangeData, status }),
@@ -83,7 +147,7 @@ describe('GiftExchangePage', () => {
83147
json: async () => mockMembers,
84148
})
85149
.mockResolvedValueOnce({
86-
json: async () => mockGiftSuggestions,
150+
json: async () => suggestions,
87151
});
88152
};
89153

@@ -182,4 +246,35 @@ describe('GiftExchangePage', () => {
182246
});
183247
});
184248
});
249+
250+
it('renders the WaitingForSuggestions component when no gift suggestions are available', async () => {
251+
(useAuthContext as jest.Mock).mockReturnValue(mockMemberSession);
252+
mockDataFetch('active');
253+
render(<GiftExchangePage />);
254+
255+
const waitingForSuggestions = await screen.findByTestId('suggestions-waiting');
256+
expect(waitingForSuggestions).toBeInTheDocument();
257+
258+
const giftSuggestionCards = screen.queryAllByTestId('gift-suggestion-card');
259+
expect(giftSuggestionCards.length).toBe(0);
260+
});
261+
262+
it('renders 3 GiftSuggestionCard components when 3 gift suggestions are available', async () => {
263+
(useAuthContext as jest.Mock).mockReturnValue(mockMemberSession);
264+
mockDataFetch('active', mockGiftSuggestionsFull);
265+
render(<GiftExchangePage />);
266+
267+
// Verify the waiting for suggestions component is not rendering
268+
const waitingForSuggestions = screen.queryByTestId('suggestions-waiting');
269+
expect(waitingForSuggestions).not.toBeInTheDocument();
270+
271+
// Verify 3 gift suggestions are provided
272+
const giftSuggestionCards = await screen.findAllByTestId('gift-suggestion-card');
273+
expect(giftSuggestionCards.length).toBe(3);
274+
275+
// Verify each card is in the document
276+
giftSuggestionCards.forEach(card => {
277+
expect(card).toBeInTheDocument();
278+
});
279+
});
185280
});

app/gift-exchanges/[id]/page.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { WaitingForSuggestions } from './WaitingForSuggestions/WaitingForSuggest
2020
import { useToast } from '@/hooks/use-toast';
2121
import { signInWithGoogle } from '@/lib/utils';
2222
import { TOASTS } from '@/components/Toast/toasts.consts';
23+
import { createClient } from '@/lib/supabase/client';
2324

2425
export default function GiftExchangePage() {
2526
const { id } = useParams();
@@ -50,6 +51,45 @@ export default function GiftExchangePage() {
5051
: false;
5152
const inviteLink = window.location.href; // Invite Link for Invite Card
5253

54+
useEffect(() => {
55+
if (!session?.user.id || !id) {
56+
return;
57+
}
58+
59+
const supabase = createClient();
60+
61+
const channel = supabase
62+
.channel('gift_suggestions_live')
63+
.on(
64+
'postgres_changes',
65+
{
66+
event: 'INSERT',
67+
schema: 'public',
68+
table: 'gift_suggestions',
69+
filter: `gift_exchange_id=eq.${id}`,
70+
},
71+
(payload) => {
72+
if (payload.new.giver_id !== session.user.id) {
73+
return;
74+
}
75+
76+
const newSuggestion: IGiftSuggestion = {
77+
...payload.new.suggestion,
78+
id: String(payload.new.id),
79+
};
80+
81+
setGiftSuggestions((prev) =>
82+
prev.some(s => s.id === newSuggestion.id) ? prev : [...prev, newSuggestion]
83+
);
84+
},
85+
)
86+
.subscribe();
87+
88+
return () => {
89+
supabase.removeChannel(channel);
90+
};
91+
}, [id, session?.user.id]);
92+
5393
const handleGiftUpdate = (
5494
updatedGift: IGiftSuggestion,
5595
originalIndex: number,
@@ -160,6 +200,7 @@ export default function GiftExchangePage() {
160200
</section>
161201
<section className="flex flex-col">
162202
<h1 className="font-bold">Gift Suggestions</h1>
203+
163204
{giftSuggestions?.length === 0 && <WaitingForSuggestions />}
164205

165206
{giftSuggestions?.length !== 0 && (

components/GiftSuggestionCard/GiftSuggestionCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const GiftSuggestionCard = ({
2828
};
2929

3030
return (
31-
<Card className="bg-giftSuggestionsCardBackground h-100 w-80 flex flex-col justify-between m-5">
31+
<Card className="bg-giftSuggestionsCardBackground h-100 w-80 flex flex-col justify-between m-5" data-testid="gift-suggestion-card">
3232
{isShowingFeedback ? (
3333
<FeedbackView
3434
allGiftSuggestions={allGiftSuggestions}

0 commit comments

Comments
 (0)