@@ -2,6 +2,16 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/react"
22import { describe , it , expect , vi } from "vitest"
33import { App } from "./App"
44
5+ // jsdom cannot rasterize DOM nodes; return a stub canvas so export flows
6+ // can be exercised end-to-end.
7+ vi . mock ( "html-to-image" , ( ) => ( {
8+ toCanvas : vi . fn ( async ( ) => ( {
9+ toDataURL : ( ) => "data:image/png;base64,AAAA" ,
10+ toBlob : ( cb : ( b : Blob | null ) => void ) =>
11+ cb ( new Blob ( [ "x" ] , { type : "image/png" } ) ) ,
12+ } ) ) ,
13+ } ) )
14+
515describe ( "App" , ( ) => {
616 it ( "renders the app title" , ( ) => {
717 render ( < App /> )
@@ -170,8 +180,7 @@ describe("reactions & replies", () => {
170180
171181 it ( "adds a reply that renders the quoted message inside the bubble" , ( ) => {
172182 render ( < App /> )
173- // Default sender is the contact, so the only combobox is "Reply to".
174- const replySelect = screen . getByRole ( "combobox" )
183+ const replySelect = screen . getByLabelText ( "Reply to message" )
175184 const options = replySelect . querySelectorAll ( "option" )
176185 // options[0] is "None"; options[1] is the first quotable message.
177186 fireEvent . change ( replySelect , { target : { value : options [ 1 ] . value } } )
@@ -216,7 +225,9 @@ describe("reactions & replies", () => {
216225 fireEvent . click ( screen . getByRole ( "button" , { name : "System" } ) )
217226
218227 expect ( screen . queryByPlaceholderText ( "👍 ❤️ 😂" ) ) . not . toBeInTheDocument ( )
219- expect ( screen . queryByRole ( "combobox" ) ) . not . toBeInTheDocument ( )
228+ expect (
229+ screen . queryByLabelText ( "Reply to message" ) ,
230+ ) . not . toBeInTheDocument ( )
220231 } )
221232} )
222233
@@ -293,6 +304,54 @@ describe("media messages", () => {
293304 } )
294305} )
295306
307+ describe ( "export options" , ( ) => {
308+ it ( "downloads at the selected format and scale" , async ( ) => {
309+ const { toCanvas } = await import ( "html-to-image" )
310+ const clickSpy = vi
311+ . spyOn ( HTMLAnchorElement . prototype , "click" )
312+ . mockImplementation ( ( ) => { } )
313+ render ( < App /> )
314+
315+ fireEvent . change ( screen . getByLabelText ( "Export format" ) , {
316+ target : { value : "webp" } ,
317+ } )
318+ fireEvent . change ( screen . getByLabelText ( "Export scale" ) , {
319+ target : { value : "3" } ,
320+ } )
321+ fireEvent . click ( screen . getByRole ( "button" , { name : "Download WEBP" } ) )
322+
323+ await waitFor ( ( ) => expect ( clickSpy ) . toHaveBeenCalled ( ) )
324+ expect ( toCanvas ) . toHaveBeenCalledWith (
325+ expect . anything ( ) ,
326+ expect . objectContaining ( { pixelRatio : 3 } ) ,
327+ )
328+ clickSpy . mockRestore ( )
329+ } )
330+
331+ it ( "copies a PNG to the clipboard" , async ( ) => {
332+ const write = vi . fn ( async ( ) => { } )
333+ vi . stubGlobal ( "navigator" , { ...navigator , clipboard : { write } } )
334+ vi . stubGlobal (
335+ "ClipboardItem" ,
336+ class {
337+ items : Record < string , Blob >
338+ constructor ( items : Record < string , Blob > ) {
339+ this . items = items
340+ }
341+ } ,
342+ )
343+ try {
344+ render ( < App /> )
345+
346+ fireEvent . click ( screen . getByRole ( "button" , { name : "Copy" } ) )
347+
348+ await waitFor ( ( ) => expect ( write ) . toHaveBeenCalled ( ) )
349+ } finally {
350+ vi . unstubAllGlobals ( )
351+ }
352+ } )
353+ } )
354+
296355describe ( "voice notes" , ( ) => {
297356 it ( "adds a voice note without text and renders duration in the bubble" , ( ) => {
298357 render ( < App /> )
0 commit comments