1- import { render , screen , fireEvent , waitFor } from "@testing-library/react"
2- import { describe , it , expect , vi , beforeEach } from "vitest"
1+ import { render , screen , fireEvent , waitFor , act } from "@testing-library/react"
2+ import { describe , it , expect , vi , beforeEach , afterEach } from "vitest"
33import DismissibleUpsell from "../DismissibleUpsell"
44
55// Mock the vscode API
@@ -10,14 +10,32 @@ vi.mock("@src/utils/vscode", () => ({
1010 } ,
1111} ) )
1212
13+ // Mock the translation hook
14+ vi . mock ( "@src/i18n/TranslationContext" , ( ) => ( {
15+ useAppTranslation : ( ) => ( {
16+ t : ( key : string ) => {
17+ const translations : Record < string , string > = {
18+ "common:dismiss" : "Dismiss" ,
19+ "common:dismissAndDontShowAgain" : "Dismiss and don't show again" ,
20+ }
21+ return translations [ key ] || key
22+ } ,
23+ } ) ,
24+ } ) )
25+
1326describe ( "DismissibleUpsell" , ( ) => {
1427 beforeEach ( ( ) => {
1528 mockPostMessage . mockClear ( )
29+ vi . clearAllTimers ( )
30+ } )
31+
32+ afterEach ( ( ) => {
33+ vi . clearAllTimers ( )
1634 } )
1735
1836 it ( "renders children content" , ( ) => {
1937 render (
20- < DismissibleUpsell className = "test-upsell" >
38+ < DismissibleUpsell id = "test-upsell" >
2139 < div > Test content</ div >
2240 </ DismissibleUpsell > ,
2341 )
@@ -27,7 +45,7 @@ describe("DismissibleUpsell", () => {
2745
2846 it ( "applies the correct variant styles" , ( ) => {
2947 const { container, rerender } = render (
30- < DismissibleUpsell className = "test-upsell" variant = "banner" >
48+ < DismissibleUpsell id = "test-upsell" variant = "banner" >
3149 < div > Banner content</ div >
3250 </ DismissibleUpsell > ,
3351 )
@@ -41,7 +59,7 @@ describe("DismissibleUpsell", () => {
4159
4260 // Re-render with default variant
4361 rerender (
44- < DismissibleUpsell className = "test-upsell" variant = "default" >
62+ < DismissibleUpsell id = "test-upsell" variant = "default" >
4563 < div > Default content</ div >
4664 </ DismissibleUpsell > ,
4765 )
@@ -55,7 +73,7 @@ describe("DismissibleUpsell", () => {
5573
5674 it ( "requests dismissed upsells list on mount" , ( ) => {
5775 render (
58- < DismissibleUpsell className = "test-upsell" >
76+ < DismissibleUpsell id = "test-upsell" >
5977 < div > Test content</ div >
6078 </ DismissibleUpsell > ,
6179 )
@@ -68,7 +86,7 @@ describe("DismissibleUpsell", () => {
6886 it ( "hides the upsell when dismiss button is clicked" , async ( ) => {
6987 const onDismiss = vi . fn ( )
7088 const { container } = render (
71- < DismissibleUpsell className = "test-upsell" onDismiss = { onDismiss } >
89+ < DismissibleUpsell id = "test-upsell" onDismiss = { onDismiss } >
7290 < div > Test content</ div >
7391 </ DismissibleUpsell > ,
7492 )
@@ -77,24 +95,24 @@ describe("DismissibleUpsell", () => {
7795 const dismissButton = screen . getByRole ( "button" , { name : / d i s m i s s / i } )
7896 fireEvent . click ( dismissButton )
7997
80- // Check that the component is no longer visible
81- await waitFor ( ( ) => {
82- expect ( container . firstChild ) . toBeNull ( )
83- } )
84-
85- // Check that the dismiss message was sent
98+ // Check that the dismiss message was sent BEFORE hiding
8699 expect ( mockPostMessage ) . toHaveBeenCalledWith ( {
87100 type : "dismissUpsell" ,
88101 upsellId : "test-upsell" ,
89102 } )
90103
104+ // Check that the component is no longer visible
105+ await waitFor ( ( ) => {
106+ expect ( container . firstChild ) . toBeNull ( )
107+ } )
108+
91109 // Check that the callback was called
92110 expect ( onDismiss ) . toHaveBeenCalled ( )
93111 } )
94112
95113 it ( "hides the upsell if it's in the dismissed list" , async ( ) => {
96114 const { container } = render (
97- < DismissibleUpsell className = "test-upsell" >
115+ < DismissibleUpsell id = "test-upsell" >
98116 < div > Test content</ div >
99117 </ DismissibleUpsell > ,
100118 )
@@ -116,7 +134,7 @@ describe("DismissibleUpsell", () => {
116134
117135 it ( "remains visible if not in the dismissed list" , async ( ) => {
118136 render (
119- < DismissibleUpsell className = "test-upsell" >
137+ < DismissibleUpsell id = "test-upsell" >
120138 < div > Test content</ div >
121139 </ DismissibleUpsell > ,
122140 )
@@ -138,7 +156,7 @@ describe("DismissibleUpsell", () => {
138156
139157 it ( "applies the className prop to the container" , ( ) => {
140158 const { container } = render (
141- < DismissibleUpsell className = "custom-class" >
159+ < DismissibleUpsell id = "test-upsell" className = "custom-class" >
142160 < div > Test content</ div >
143161 </ DismissibleUpsell > ,
144162 )
@@ -148,7 +166,7 @@ describe("DismissibleUpsell", () => {
148166
149167 it ( "dismiss button has proper accessibility attributes" , ( ) => {
150168 render (
151- < DismissibleUpsell className = "test-upsell" >
169+ < DismissibleUpsell id = "test-upsell" >
152170 < div > Test content</ div >
153171 </ DismissibleUpsell > ,
154172 )
@@ -157,4 +175,131 @@ describe("DismissibleUpsell", () => {
157175 expect ( dismissButton ) . toHaveAttribute ( "aria-label" , "Dismiss" )
158176 expect ( dismissButton ) . toHaveAttribute ( "title" , "Dismiss and don't show again" )
159177 } )
178+
179+ // New edge case tests
180+ it ( "handles multiple rapid dismissals of the same component" , async ( ) => {
181+ const onDismiss = vi . fn ( )
182+ render (
183+ < DismissibleUpsell id = "test-upsell" onDismiss = { onDismiss } >
184+ < div > Test content</ div >
185+ </ DismissibleUpsell > ,
186+ )
187+
188+ const dismissButton = screen . getByRole ( "button" , { name : / d i s m i s s / i } )
189+
190+ // Click multiple times rapidly
191+ fireEvent . click ( dismissButton )
192+ fireEvent . click ( dismissButton )
193+ fireEvent . click ( dismissButton )
194+
195+ // Should only send one message
196+ expect ( mockPostMessage ) . toHaveBeenCalledTimes ( 2 ) // 1 for getDismissedUpsells, 1 for dismissUpsell
197+ expect ( mockPostMessage ) . toHaveBeenCalledWith ( {
198+ type : "dismissUpsell" ,
199+ upsellId : "test-upsell" ,
200+ } )
201+
202+ // Callback should only be called once
203+ expect ( onDismiss ) . toHaveBeenCalledTimes ( 1 )
204+ } )
205+
206+ it ( "does not update state after component unmounts" , async ( ) => {
207+ const { unmount } = render (
208+ < DismissibleUpsell id = "test-upsell" >
209+ < div > Test content</ div >
210+ </ DismissibleUpsell > ,
211+ )
212+
213+ // Unmount the component
214+ unmount ( )
215+
216+ // Simulate receiving a message after unmount
217+ const messageEvent = new MessageEvent ( "message" , {
218+ data : {
219+ type : "dismissedUpsells" ,
220+ list : [ "test-upsell" ] ,
221+ } ,
222+ } )
223+
224+ // This should not cause any errors
225+ act ( ( ) => {
226+ window . dispatchEvent ( messageEvent )
227+ } )
228+
229+ // No errors should be thrown
230+ expect ( true ) . toBe ( true )
231+ } )
232+
233+ it ( "handles invalid/malformed messages gracefully" , ( ) => {
234+ render (
235+ < DismissibleUpsell id = "test-upsell" >
236+ < div > Test content</ div >
237+ </ DismissibleUpsell > ,
238+ )
239+
240+ // Send various malformed messages
241+ const malformedMessages = [
242+ { type : "dismissedUpsells" , list : null } ,
243+ { type : "dismissedUpsells" , list : "not-an-array" } ,
244+ { type : "dismissedUpsells" } , // missing list
245+ { type : "wrongType" , list : [ "test-upsell" ] } ,
246+ null ,
247+ undefined ,
248+ "string-message" ,
249+ ]
250+
251+ malformedMessages . forEach ( ( data ) => {
252+ const messageEvent = new MessageEvent ( "message" , { data } )
253+ window . dispatchEvent ( messageEvent )
254+ } )
255+
256+ // Component should still be visible
257+ expect ( screen . getByText ( "Test content" ) ) . toBeInTheDocument ( )
258+ } )
259+
260+ it ( "ensures message is sent before component unmounts on dismiss" , async ( ) => {
261+ const { unmount } = render (
262+ < DismissibleUpsell id = "test-upsell" >
263+ < div > Test content</ div >
264+ </ DismissibleUpsell > ,
265+ )
266+
267+ const dismissButton = screen . getByRole ( "button" , { name : / d i s m i s s / i } )
268+ fireEvent . click ( dismissButton )
269+
270+ // Message should be sent immediately
271+ expect ( mockPostMessage ) . toHaveBeenCalledWith ( {
272+ type : "dismissUpsell" ,
273+ upsellId : "test-upsell" ,
274+ } )
275+
276+ // Unmount immediately after clicking
277+ unmount ( )
278+
279+ // Message was already sent before unmount
280+ expect ( mockPostMessage ) . toHaveBeenCalledWith ( {
281+ type : "dismissUpsell" ,
282+ upsellId : "test-upsell" ,
283+ } )
284+ } )
285+
286+ it ( "uses separate id and className props correctly" , ( ) => {
287+ const { container } = render (
288+ < DismissibleUpsell id = "unique-id" className = "styling-class" >
289+ < div > Test content</ div >
290+ </ DismissibleUpsell > ,
291+ )
292+
293+ // className should be applied to the container
294+ expect ( container . firstChild ) . toHaveClass ( "styling-class" )
295+
296+ // When dismissed, should use the id, not className
297+ const dismissButton = screen . getByRole ( "button" , { name : / d i s m i s s / i } )
298+ fireEvent . click ( dismissButton )
299+
300+ expect ( mockPostMessage ) . toHaveBeenCalledWith ( {
301+ type : "dismissUpsell" ,
302+ upsellId : "unique-id" ,
303+ } )
304+ } )
160305} )
0 commit comments