11// @vitest -environment node
2- import { afterAll , afterEach , beforeAll , beforeEach , describe , expect , test , vi } from 'vitest'
3- import { GlobalRegistrator } from '@happy-dom/global-registrator'
4- import { experimental_AstroContainer as AstroContainer } from 'astro/container'
5- import {
6- getDownloadButtonWrapper ,
7- getDownloadCompanyNameInput ,
8- getDownloadFirstNameInput ,
9- getDownloadFormElement ,
10- getDownloadJobTitleInput ,
11- getDownloadLastNameInput ,
12- getDownloadStatusDiv ,
13- getDownloadSubmitButton ,
14- getDownloadWorkEmailInput ,
15- } from '@components/Forms/Download/client/selectors'
16- import DownloadFormComponent from '@components/Forms/Download/index.astro'
17- import { TestError } from '@test/errors'
18- import { withLitRuntime } from '@test/unit/helpers/litRuntime'
2+ import { afterEach , describe , expect , it , vi } from 'vitest'
3+ import { renderDownloadForm , type DownloadFormElements } from './testUtils'
194
205// Mock the logger to suppress error output in tests
216vi . mock ( '@lib/logger' , ( ) => ( {
@@ -26,254 +11,136 @@ vi.mock('@lib/logger', () => ({
2611 debug : vi . fn ( ) ,
2712 } ,
2813} ) )
29-
30- let container : AstroContainer | undefined
31- let consoleErrorSpy : ReturnType < typeof vi . spyOn > | undefined
32- const originalFetch = global . fetch
33- let downloadFormClientPromise : Promise < void > | undefined
34- const downloadFormTagName = 'download-form'
35-
36- /**
37- * Helper function to set up DOM from Container API
38- */
39- async function renderDownloadFormDOM ( ) {
40- if ( ! container ) {
41- throw new TestError ( 'Astro container not initialized' )
42- }
43-
44- const result = await container . renderToString ( DownloadFormComponent , {
45- props : {
46- title : 'Test Resource' ,
47- fileName : 'test-file.pdf' ,
48- fileType : 'PDF' ,
49- } ,
50- } )
51-
52- const template = document . createElement ( 'template' )
53- template . innerHTML = result
54- const body = document . body
55- body . replaceChildren ( )
56- while ( template . content . firstChild ) {
57- body . appendChild ( template . content . firstChild )
58- }
14+ const flushPromises = ( ) => new Promise ( resolve => setTimeout ( resolve , 0 ) )
15+
16+ const defaultFormValues = {
17+ firstName : 'Jane' ,
18+ lastName : 'Doe' ,
19+ workEmail : 'jane@example.com' ,
20+ jobTitle : 'Engineer' ,
21+ companyName : 'Acme Corp' ,
5922}
6023
61- async function hydrateDownloadFormElement ( ) {
62- await customElements . whenDefined ( downloadFormTagName )
63- const element = document . querySelector ( downloadFormTagName )
64- if ( ! element ) {
65- throw new TestError ( 'download-form element not found in DOM' )
66- }
67- await Promise . resolve ( )
68- return element
24+ const fillDownloadForm = (
25+ elements : DownloadFormElements ,
26+ overrides : Partial < typeof defaultFormValues > = { } ,
27+ ) => {
28+ const values = { ...defaultFormValues , ...overrides }
29+ elements . firstName . value = values . firstName
30+ elements . lastName . value = values . lastName
31+ elements . workEmail . value = values . workEmail
32+ elements . jobTitle . value = values . jobTitle
33+ elements . companyName . value = values . companyName
34+ return values
6935}
7036
71- const withHydratedDownloadForm = async ( assertions : ( ) => Promise < void > | void ) => {
72- await withLitRuntime ( async ( { register } ) => {
73- await register ( downloadFormTagName , async ( ) => ensureDownloadFormClient ( ) )
74- await renderDownloadFormDOM ( )
75- await hydrateDownloadFormElement ( )
76- await assertions ( )
77- } )
37+ const submitForm = ( window : Window & typeof globalThis , form : HTMLFormElement ) => {
38+ const submitEvent = new window . Event ( 'submit' , { bubbles : true , cancelable : true } )
39+ form . dispatchEvent ( submitEvent )
7840}
7941
80- describe ( 'download-form web component' , ( ) => {
81- beforeAll ( ( ) => {
82- GlobalRegistrator . register ( )
83- } )
84-
85- afterAll ( async ( ) => {
86- await GlobalRegistrator . unregister ( )
87- } )
88-
89- beforeEach ( async ( ) => {
90- container = await AstroContainer . create ( )
91- global . fetch = vi . fn ( ) . mockResolvedValue ( {
92- ok : true ,
93- json : async ( ) => ( { success : true } ) ,
94- } ) as typeof fetch
95-
96- consoleErrorSpy = vi . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } )
97- } )
42+ const successfulResponse = ( ) : Response =>
43+ ( {
44+ ok : true ,
45+ json : async ( ) => ( { success : true } ) ,
46+ } as Response )
9847
48+ describe ( 'download-form web component' , ( ) => {
9949 afterEach ( ( ) => {
100- consoleErrorSpy ?. mockRestore ( )
101- if ( originalFetch ) {
102- global . fetch = originalFetch
103- } else {
104- delete ( global as typeof global & { fetch ?: typeof global . fetch } ) . fetch
105- }
106- container = undefined
50+ vi . restoreAllMocks ( )
10751 } )
10852
109- test ( 'submits download requests via fetch' , async ( ) => {
110- await withHydratedDownloadForm ( async ( ) => {
111- const {
112- form,
113- firstName,
114- lastName,
115- workEmail,
116- jobTitle,
117- companyName,
118- } = getDownloadFormElements ( )
119- firstName . value = 'John'
120- lastName . value = 'Doe'
121- workEmail . value = 'john.doe@example.com'
122- jobTitle . value = 'Developer'
123- companyName . value = 'Acme Corp'
53+ it ( 'submits download requests via fetch' , async ( ) => {
54+ await renderDownloadForm ( async ( { elements, window } ) => {
55+ const fetchSpy = vi . spyOn ( globalThis , 'fetch' ) . mockResolvedValue ( successfulResponse ( ) )
56+ const payload = fillDownloadForm ( elements )
12457
125- await submitFormAndFlush ( form )
58+ submitForm ( window , elements . form )
59+ await flushPromises ( )
12660
127- expect ( global . fetch ) . toHaveBeenCalledWith (
61+ expect ( fetchSpy ) . toHaveBeenCalledWith (
12862 '/api/downloads/submit' ,
12963 expect . objectContaining ( {
13064 method : 'POST' ,
13165 headers : {
13266 'Content-Type' : 'application/json' ,
13367 } ,
134- body : JSON . stringify ( {
135- firstName : 'John' ,
136- lastName : 'Doe' ,
137- workEmail : 'john.doe@example.com' ,
138- jobTitle : 'Developer' ,
139- companyName : 'Acme Corp' ,
140- } ) ,
141- } )
68+ body : JSON . stringify ( payload ) ,
69+ } ) ,
14270 )
71+
72+ fetchSpy . mockRestore ( )
14373 } )
14474 } )
14575
146- test ( 'shows success message and reveals download button' , async ( ) => {
147- await withHydratedDownloadForm ( async ( ) => {
148- const {
149- form,
150- firstName,
151- lastName,
152- workEmail,
153- jobTitle,
154- companyName,
155- } = getDownloadFormElements ( )
156- firstName . value = 'Jane'
157- lastName . value = 'Doe'
158- workEmail . value = 'jane@example.com'
159- jobTitle . value = 'Engineer'
160- companyName . value = 'Widgets Inc'
76+ it ( 'shows success message and reveals download button' , async ( ) => {
77+ await renderDownloadForm ( async ( { elements, window } ) => {
78+ const fetchSpy = vi . spyOn ( globalThis , 'fetch' ) . mockResolvedValue ( successfulResponse ( ) )
79+ fillDownloadForm ( elements )
16180
162- await submitFormAndFlush ( form )
81+ submitForm ( window , elements . form )
82+ await flushPromises ( )
16383
164- const statusDiv = getDownloadStatusDiv ( )
165- expect ( statusDiv ?. classList . contains ( 'hidden' ) ) . toBe ( false )
166- expect ( statusDiv ?. classList . contains ( 'success' ) ) . toBe ( true )
167- expect ( statusDiv ?. textContent ) . toContain ( 'Thank you' )
84+ expect ( elements . statusDiv . classList . contains ( 'hidden' ) ) . toBe ( false )
85+ expect ( elements . statusDiv . classList . contains ( 'success' ) ) . toBe ( true )
86+ expect ( elements . statusDiv . textContent ) . toContain ( 'Thank you' )
87+ expect ( elements . downloadButtonWrapper . classList . contains ( 'hidden' ) ) . toBe ( false )
88+ expect ( elements . submitButton . classList . contains ( 'hidden' ) ) . toBe ( true )
89+ expect ( elements . firstName . value ) . toBe ( '' )
90+ expect ( elements . lastName . value ) . toBe ( '' )
91+ expect ( elements . workEmail . value ) . toBe ( '' )
16892
169- const downloadWrapper = getDownloadButtonWrapper ( )
170- expect ( downloadWrapper ?. classList . contains ( 'hidden' ) ) . toBe ( false )
171-
172- const submitButton = getDownloadSubmitButton ( )
173- expect ( submitButton ?. classList . contains ( 'hidden' ) ) . toBe ( true )
93+ fetchSpy . mockRestore ( )
17494 } )
17595 } )
17696
177- test ( 'displays error state when API fails' , async ( ) => {
178- ; ( global . fetch as ReturnType < typeof vi . fn > ) . mockResolvedValueOnce ( {
179- ok : false ,
180- json : async ( ) => ( { error : 'Server error' } ) ,
181- } )
182-
183- await withHydratedDownloadForm ( async ( ) => {
184- const {
185- form,
186- firstName,
187- lastName,
188- workEmail,
189- jobTitle,
190- companyName,
191- } = getDownloadFormElements ( )
192- firstName . value = 'Sam'
193- lastName . value = 'Lee'
194- workEmail . value = 'sam@example.com'
195- jobTitle . value = 'Analyst'
196- companyName . value = 'Example'
197-
198- await submitFormAndFlush ( form )
199-
200- const statusDiv = getDownloadStatusDiv ( )
201- expect ( statusDiv ?. classList . contains ( 'hidden' ) ) . toBe ( false )
202- expect ( statusDiv ?. classList . contains ( 'error' ) ) . toBe ( true )
203- expect ( statusDiv ?. textContent ) . toContain ( 'error' )
97+ it ( 'displays error state when API fails' , async ( ) => {
98+ await renderDownloadForm ( async ( { elements, window } ) => {
99+ const fetchSpy = vi . spyOn ( globalThis , 'fetch' ) . mockResolvedValue ( {
100+ ok : false ,
101+ json : async ( ) => ( { message : 'Server error' } ) ,
102+ } as Response )
103+
104+ fillDownloadForm ( elements )
105+ submitForm ( window , elements . form )
106+ await flushPromises ( )
107+
108+ expect ( fetchSpy ) . toHaveBeenCalled ( )
109+ expect ( elements . statusDiv . classList . contains ( 'hidden' ) ) . toBe ( false )
110+ expect ( elements . statusDiv . classList . contains ( 'error' ) ) . toBe ( true )
111+ expect ( elements . statusDiv . textContent ) . toContain ( 'There was an error processing your request' )
112+ expect ( elements . downloadButtonWrapper . classList . contains ( 'hidden' ) ) . toBe ( true )
113+ expect ( elements . submitButton . classList . contains ( 'hidden' ) ) . toBe ( false )
114+
115+ fetchSpy . mockRestore ( )
204116 } )
205117 } )
206118
207- test ( 'disables submit button while request is pending' , async ( ) => {
208- let resolveFetch : ( ( ) => void ) | undefined
209- ; ( global . fetch as ReturnType < typeof vi . fn > ) . mockImplementationOnce (
210- ( ) =>
211- new Promise ( ( resolve ) => {
212- resolveFetch = ( ) =>
213- resolve ( {
214- ok : true ,
215- json : async ( ) => ( { success : true } ) ,
216- } )
217- } )
218- )
219-
220- await withHydratedDownloadForm ( async ( ) => {
221- const {
222- form,
223- firstName,
224- lastName,
225- workEmail,
226- jobTitle,
227- companyName,
228- } = getDownloadFormElements ( )
229- firstName . value = 'Ava'
230- lastName . value = 'Jones'
231- workEmail . value = 'ava@example.com'
232- jobTitle . value = 'PM'
233- companyName . value = 'Acme'
234-
235- const submitButton = getDownloadSubmitButton ( )
119+ it ( 'disables submit button while request is pending' , async ( ) => {
120+ await renderDownloadForm ( async ( { elements, window } ) => {
121+ let resolveFetch : ( ( ) => void ) | undefined
122+ const fetchSpy = vi . spyOn ( globalThis , 'fetch' ) . mockImplementation (
123+ ( ) =>
124+ new Promise ( resolve => {
125+ resolveFetch = ( ) => resolve ( successfulResponse ( ) )
126+ } ) ,
127+ )
236128
237- const submitEvent = new Event ( 'submit' , { bubbles : true , cancelable : true } )
238- form . dispatchEvent ( submitEvent )
129+ fillDownloadForm ( elements )
130+ submitForm ( window , elements . form )
239131
240- expect ( submitButton . disabled ) . toBe ( true )
241- expect ( submitButton . textContent ) . toBe ( 'Processing...' )
132+ expect ( elements . submitButton . disabled ) . toBe ( true )
133+ expect ( elements . submitButton . textContent ) . toBe ( 'Processing...' )
242134
243135 resolveFetch ?.( )
244- await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
136+ await flushPromises ( )
245137
246- expect ( submitButton . disabled ) . toBe ( false )
247- expect ( submitButton . textContent ) . toBe ( 'Download Now' )
138+ expect ( fetchSpy ) . toHaveBeenCalled ( )
139+ expect ( elements . submitButton . disabled ) . toBe ( false )
140+ expect ( elements . submitButton . textContent ) . toBe ( 'Download Now' )
141+
142+ fetchSpy . mockRestore ( )
248143 } )
249144 } )
250145} )
251146
252- async function ensureDownloadFormClient ( ) {
253- if ( ! downloadFormClientPromise ) {
254- downloadFormClientPromise = import ( '@components/Forms/Download/client' ) . then (
255- ( { registerDownloadFormWebComponent } ) => {
256- registerDownloadFormWebComponent ( )
257- }
258- )
259- }
260- return downloadFormClientPromise
261- }
262-
263- function getDownloadFormElements ( ) {
264- return {
265- form : getDownloadFormElement ( ) ,
266- firstName : getDownloadFirstNameInput ( ) ,
267- lastName : getDownloadLastNameInput ( ) ,
268- workEmail : getDownloadWorkEmailInput ( ) ,
269- jobTitle : getDownloadJobTitleInput ( ) ,
270- companyName : getDownloadCompanyNameInput ( ) ,
271- }
272- }
273-
274- async function submitFormAndFlush ( form : HTMLFormElement ) {
275- const submitEvent = new Event ( 'submit' , { bubbles : true , cancelable : true } )
276- form . dispatchEvent ( submitEvent )
277- await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
278- }
279-
0 commit comments