@@ -53,6 +53,14 @@ const userPage = ilha.render(() => {
5353} ) ;
5454const notFound = ilha . render ( ( ) => `<p>404</p>` ) ;
5555
56+ // shared registry used across hydratable tests
57+ const registry : Record < string , typeof homePage > = {
58+ home : homePage ,
59+ about : aboutPage ,
60+ user : userPage ,
61+ notFound : notFound ,
62+ } ;
63+
5664// ─────────────────────────────────────────────
5765// route matching
5866// ─────────────────────────────────────────────
@@ -614,3 +622,120 @@ describe("SSR render()", () => {
614622 expect ( search ( ) ) . toBe ( "?sort=asc" ) ;
615623 } ) ;
616624} ) ;
625+
626+ // ─────────────────────────────────────────────
627+ // SSR — router().renderHydratable()
628+ // ─────────────────────────────────────────────
629+
630+ describe ( "SSR renderHydratable()" , ( ) => {
631+ it ( "returns a string (is async)" , async ( ) => {
632+ const html = await router ( )
633+ . route ( "/" , homePage )
634+ . route ( "/**" , notFound )
635+ . renderHydratable ( "/" , registry ) ;
636+ expect ( typeof html ) . toBe ( "string" ) ;
637+ } ) ;
638+
639+ it ( "wraps output in data-router-view" , async ( ) => {
640+ const html = await router ( ) . route ( "/" , homePage ) . renderHydratable ( "/" , registry ) ;
641+ expect ( html ) . toContain ( "data-router-view" ) ;
642+ } ) ;
643+
644+ it ( "includes data-ilha attribute with the island name" , async ( ) => {
645+ const html = await router ( ) . route ( "/" , homePage ) . renderHydratable ( "/" , registry ) ;
646+ expect ( html ) . toContain ( `data-ilha="home"` ) ;
647+ } ) ;
648+
649+ it ( "includes island content in the output" , async ( ) => {
650+ const html = await router ( ) . route ( "/" , homePage ) . renderHydratable ( "/" , registry ) ;
651+ expect ( html ) . toContain ( "home" ) ;
652+ } ) ;
653+
654+ it ( "resolves correct island for /about" , async ( ) => {
655+ const html = await router ( )
656+ . route ( "/" , homePage )
657+ . route ( "/about" , aboutPage )
658+ . route ( "/**" , notFound )
659+ . renderHydratable ( "/about" , registry ) ;
660+ expect ( html ) . toContain ( `data-ilha="about"` ) ;
661+ expect ( html ) . toContain ( "about" ) ;
662+ expect ( html ) . not . toContain ( `data-ilha="home"` ) ;
663+ } ) ;
664+
665+ it ( "renders data-router-empty when no route matches" , async ( ) => {
666+ const html = await router ( ) . route ( "/" , homePage ) . renderHydratable ( "/unknown" , registry ) ;
667+ expect ( html ) . toContain ( "data-router-empty" ) ;
668+ expect ( html ) . not . toContain ( "data-ilha" ) ;
669+ } ) ;
670+
671+ it ( "populates route signals identically to render()" , async ( ) => {
672+ await router ( ) . route ( "/user/:id" , userPage ) . renderHydratable ( "/user/42" , registry ) ;
673+ expect ( routePath ( ) ) . toBe ( "/user/42" ) ;
674+ expect ( routeParams ( ) ) . toEqual ( { id : "42" } ) ;
675+ } ) ;
676+
677+ it ( "populates routeSearch signal" , async ( ) => {
678+ await router ( ) . route ( "/about" , aboutPage ) . renderHydratable ( "/about?tab=docs" , registry ) ;
679+ expect ( routeSearch ( ) ) . toBe ( "?tab=docs" ) ;
680+ } ) ;
681+
682+ it ( "accepts a full URL string" , async ( ) => {
683+ const html = await router ( )
684+ . route ( "/about" , aboutPage )
685+ . renderHydratable ( "http://example.com/about" , registry ) ;
686+ expect ( html ) . toContain ( `data-ilha="about"` ) ;
687+ } ) ;
688+
689+ it ( "accepts a URL object" , async ( ) => {
690+ const html = await router ( )
691+ . route ( "/about" , aboutPage )
692+ . renderHydratable ( new URL ( "http://example.com/about" ) , registry ) ;
693+ expect ( html ) . toContain ( `data-ilha="about"` ) ;
694+ } ) ;
695+
696+ it ( "falls back to plain SSR and warns when island is not in registry" , async ( ) => {
697+ const warn = spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } ) ;
698+ const unregistered = ilha . render ( ( ) => `<p>unregistered</p>` ) ;
699+ const html = await router ( ) . route ( "/" , unregistered ) . renderHydratable ( "/" , registry ) ;
700+
701+ expect ( warn ) . toHaveBeenCalledWith ( expect . stringContaining ( "[ilha-router]" ) ) ;
702+ expect ( html ) . toContain ( "data-router-view" ) ;
703+ expect ( html ) . toContain ( "unregistered" ) ;
704+ expect ( html ) . not . toContain ( "data-ilha" ) ;
705+ warn . mockRestore ( ) ;
706+ } ) ;
707+
708+ it ( "does not include data-ilha when falling back to plain SSR" , async ( ) => {
709+ const warn = spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } ) ;
710+ const unregistered = ilha . render ( ( ) => `<p>x</p>` ) ;
711+ const html = await router ( ) . route ( "/" , unregistered ) . renderHydratable ( "/" , { } ) ;
712+ expect ( html ) . not . toContain ( "data-ilha" ) ;
713+ warn . mockRestore ( ) ;
714+ } ) ;
715+
716+ it ( "snapshot option is forwarded — data-ilha-state present" , async ( ) => {
717+ const stateful = ilha . state ( "count" , 0 ) . render ( ( ) => `<p>count</p>` ) ;
718+ const reg = { stateful } ;
719+ const html = await router ( ) . route ( "/" , stateful ) . renderHydratable ( "/" , reg , { snapshot : true } ) ;
720+ expect ( html ) . toContain ( "data-ilha-state" ) ;
721+ } ) ;
722+
723+ it ( "snapshot: false omits data-ilha-state" , async ( ) => {
724+ const stateful = ilha . state ( "count" , 0 ) . render ( ( ) => `<p>count</p>` ) ;
725+ const reg = { stateful } ;
726+ const html = await router ( )
727+ . route ( "/" , stateful )
728+ . renderHydratable ( "/" , reg , { snapshot : false } ) ;
729+ expect ( html ) . not . toContain ( "data-ilha-state" ) ;
730+ } ) ;
731+
732+ it ( "each call is independent — registry lookup uses active island" , async ( ) => {
733+ const r = router ( ) . route ( "/" , homePage ) . route ( "/about" , aboutPage ) ;
734+
735+ const h1 = await r . renderHydratable ( "/" , registry ) ;
736+ const h2 = await r . renderHydratable ( "/about" , registry ) ;
737+
738+ expect ( h1 ) . toContain ( `data-ilha="home"` ) ;
739+ expect ( h2 ) . toContain ( `data-ilha="about"` ) ;
740+ } ) ;
741+ } ) ;
0 commit comments