@@ -191,6 +191,94 @@ describe('renderToChunks', () => {
191191 ] ) ;
192192 } ) ;
193193
194+ it ( 'should inject deferred content before </body></html> for full document rendering' , async ( ) => {
195+ const { Suspender, suspended } = createSuspender ( ) ;
196+
197+ const result = [ ] ;
198+ const promise = renderToChunks (
199+ < html >
200+ < head >
201+ < title > Test</ title >
202+ </ head >
203+ < body >
204+ < Suspense fallback = "loading..." >
205+ < Suspender />
206+ </ Suspense >
207+ </ body >
208+ </ html > ,
209+ { onWrite : ( s ) => result . push ( s ) }
210+ ) ;
211+ suspended . resolve ( ) ;
212+ await promise ;
213+
214+ const fullHtml = result . join ( '' ) ;
215+
216+ // Deferred wrapper must appear before </body></html>, not after
217+ const deferredPos = fullHtml . indexOf ( '<div hidden>' ) ;
218+ const bodyClosePos = fullHtml . indexOf ( '</body>' ) ;
219+ const htmlClosePos = fullHtml . indexOf ( '</html>' ) ;
220+
221+ expect ( deferredPos ) . toBeGreaterThan ( - 1 ) ;
222+ expect ( deferredPos ) . toBeLessThan ( bodyClosePos ) ;
223+ expect ( bodyClosePos ) . toBeLessThan ( htmlClosePos ) ;
224+
225+ // The document must end with </html>
226+ expect ( fullHtml . endsWith ( '</html>' ) ) . toBe ( true ) ;
227+ // No content after </html>
228+ expect ( result [ result . length - 1 ] ) . toBe ( '</body></html>' ) ;
229+ } ) ;
230+
231+ it ( 'should prepend <!DOCTYPE html> when rendering a full document with suspended content' , async ( ) => {
232+ const { Suspender, suspended } = createSuspender ( ) ;
233+
234+ const result = [ ] ;
235+ const promise = renderToChunks (
236+ < html >
237+ < head >
238+ < title > Test</ title >
239+ </ head >
240+ < body >
241+ < Suspense fallback = "loading..." >
242+ < Suspender />
243+ </ Suspense >
244+ </ body >
245+ </ html > ,
246+ { onWrite : ( s ) => result . push ( s ) }
247+ ) ;
248+ suspended . resolve ( ) ;
249+ await promise ;
250+
251+ // The first chunk must be prefixed with <!DOCTYPE html>
252+ expect ( result [ 0 ] . startsWith ( '<!DOCTYPE html>' ) ) . toBe ( true ) ;
253+
254+ // The full output must start with the doctype
255+ const fullHtml = result . join ( '' ) ;
256+ expect ( fullHtml . startsWith ( '<!DOCTYPE html>' ) ) . toBe ( true ) ;
257+
258+ // The doctype should appear exactly once
259+ const doctypeCount = ( fullHtml . match ( / < ! D O C T Y P E h t m l > / gi) || [ ] ) . length ;
260+ expect ( doctypeCount ) . toBe ( 1 ) ;
261+ } ) ;
262+
263+ it ( 'should not prepend <!DOCTYPE html> when rendering a non-document fragment with suspended content' , async ( ) => {
264+ const { Suspender, suspended } = createSuspender ( ) ;
265+
266+ const result = [ ] ;
267+ const promise = renderToChunks (
268+ < div >
269+ < Suspense fallback = "loading..." >
270+ < Suspender />
271+ </ Suspense >
272+ </ div > ,
273+ { onWrite : ( s ) => result . push ( s ) }
274+ ) ;
275+ suspended . resolve ( ) ;
276+ await promise ;
277+
278+ const fullHtml = result . join ( '' ) ;
279+ expect ( fullHtml . includes ( '<!DOCTYPE html>' ) ) . toBe ( false ) ;
280+ } ) ;
281+
194282 it ( 'should support a component that suspends multiple times' , async ( ) => {
195283 const { Suspender, suspended } = createSuspender ( ) ;
196284 const { Suspender : Suspender2 , suspended : suspended2 } = createSuspender ( ) ;
@@ -217,10 +305,10 @@ describe('renderToChunks', () => {
217305 await promise ;
218306
219307 expect ( result ) . to . deep . equal ( [
220- '<div><!--preact-island:49 -->loading part 1...<!--/preact-island:49 --></div>' ,
308+ '<div><!--preact-island:70 -->loading part 1...<!--/preact-island:70 --></div>' ,
221309 '<div hidden>' ,
222310 createInitScript ( 1 ) ,
223- createSubtree ( '49 ' , '<p>it works</p><p>it works</p>' ) ,
311+ createSubtree ( '70 ' , '<p>it works</p><p>it works</p>' ) ,
224312 '</div>'
225313 ] ) ;
226314 } ) ;
0 commit comments